代码之家  ›  专栏  ›  技术社区  ›  matt

asyncDetached在MainActor调用后回落到主线程

  •  2
  • matt  · 技术社区  · 5 年前

    我正在试用新的异步/等待功能。我的目标是 test() 方法,所以我使用 Task.detached ; 但在 测验 我需要调用主线程,所以我使用MainActor。

    (我意识到,孤立来看,这可能看起来很复杂,但它是从一个更好的现实世界案例中精简而来的。)

    好的,所以测试代码看起来是这样的(在视图控制器中):

    override func viewDidLoad() {
        super.viewDidLoad()
        Task.detached(priority: .userInitiated) {
            await self.test()
        }
    }
    @MainActor func getBounds() async -> CGRect {
        let bounds = self.view.bounds
        return bounds
    }
    func test() async {
        print("test 1", Thread.isMainThread) // false
        let bounds = await self.getBounds()
        print("test 2", Thread.isMainThread) // true
    }
    

    第一个 print 说我不在主线上。这就是我所期望的。

    但是第二个 打印 我说 在主线程上。那个 不是 我所期望的。

    就因为我调用了MainActor函数,感觉就像我神秘地回到了主线程中。我想我会等待主线程,然后在我已经上过的后台线程中恢复。

    这是一个bug,还是我的期望错了?如果是后者,如何 我在 await 但后来又回到我的话题上来了?我以为这正是async/await可以让事情变得容易的原因。。。?

    (在某种程度上,我可以通过打电话来“解决”这个问题 任务已分离 在呼叫之后再次 getBounds ; 但在这一点上,我的代码看起来非常像嵌套的GCD,我不得不想知道为什么要使用async/await。)

    也许我还为时过早,但我还是把它作为一个bug提交了: https://bugs.swift.org/browse/SR-14756 .


    更多笔记 :

    我可以通过更换来解决问题

        let bounds = await self.getBounds()
    

    具有

        async let bounds = self.getBounds()
        let thebounds = await bounds
    

    但这似乎是不必要的精心设计,并不能让我相信最初的现象不是一个bug。


    我也可以通过使用演员来解决问题,这开始看起来是最好的方法。但同样,这并不能说服我,我在这里注意到的现象不是一个bug。


    我越来越相信这是一个bug。我刚刚遇到(并报告)以下情况:

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        async {
            print("howdy")
            await doSomeNetworking()
        }
    }
    func doSomeNetworking() async {
        print(Thread.isMainThread)
    }
    

    这个打印 howdy 然后是第二个 打印 打印 true 。但如果我们把第一张照片评论掉,剩下的(第二张) 打印 打印 false !

    仅仅添加或删除打印语句怎么能改变我们的线程?当然这不是故意的。

    0 回复  |  直到 4 年前
        1
  •  7
  •   fullsailor    5 年前

    据我所知,鉴于这一切都是全新的,无法保证 asyncDetached 必须安排离开主线程。

    Swift Concurrency: Behind the Scenes 会话中,讨论了调度程序将尝试将事情保持在同一个线程上以避免上下文切换。不过,考虑到这一点,我不知道你会如何具体避免主线程,但也许我们不应该在意,只要任务进展顺利,从不阻塞。

    我找到了时间戳(23:18),它解释了不能保证同一个线程在等待后会获得延续。 https://developer.apple.com/videos/play/wwdc2021/10254/?time=1398

        2
  •  5
  •   deaton.dg    5 年前

    即使您进行测试以找出等待的确切线程行为 @MainActor 函数,你不应该依赖它。正如@fullseller的回答一样,该语言明确地不能保证在等待后在同一个线程上恢复工作,因此这种行为可能会在任何操作系统更新中发生变化。将来,您可能可以通过使用自定义执行器来请求特定的线程,但目前该语言中还没有。看见 https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md 了解更多详细信息。

    此外,希望它不会导致在主线程上运行的任何问题。看见 https://developer.apple.com/videos/play/wwdc2021/10254/?time=2074 了解有关日程安排工作方式的详细信息。您不应该害怕通过调用 @主要参与者 函数,然后再做昂贵的工作:如果有更重要的UI工作可用,这将在您的工作之前安排,或者您的工作将在另一个线程上运行。如果您特别担心,可以使用 Task.yield() 在你昂贵的工作之前,给Swift另一个机会把你的工作从主线上转移开。看见 自愿中止 here 有关的更多详细信息 任务.yield() .

    在您的例子中,Swift很可能认为上下文切换的性能打击不值得 < 返回 从主线程,因为它已经在那里了,但如果主线程更饱和,您可能会体验到不同的行为。

    编辑

    你看到的行为 async let 是因为这会产生一个子任务,该子任务与您正在做的工作同时运行。因此,由于该子线程正在主线程上运行,所以您的其他代码不是。请参阅 https://forums.swift.org/t/concurrency-structured-concurrency/41622 有关子任务的更多详细信息。

        3
  •  3
  •   matt    4 年前

    以下公式有效,非常优雅地解决了整个问题,尽管我有点不愿意发布它,因为我真的不明白它是如何工作的:

    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            await self.test2()
        }
    }
    nonisolated func test2() async {
        print("test 1", Thread.isMainThread) // false
        let bounds = await self.view.bounds // access on main thread!
        print("test 2", bounds, Thread.isMainThread) // false
    }
    

    我已经测试了 await self.view.bounds 打电话给wazoo,两个 view 访问和 bounds 访问在主线程上。这个 nonisolated 此处的指定对于确保这一点至关重要。对此的需要以及随之而来的需要 await 对我来说非常令人惊讶,但这一切似乎都与演员的性质以及UIViewController是MainActor这一事实有关。

        4
  •  2
  •   Cristik    3 年前

    简要说明一下,自从提出这个问题以来,UIKit类被标记为 @MainActor ,所以讨论中的代码将打印出来 true 在这两种情况下。但这个问题仍然可以通过“常规”课程重现。

    现在,回到讨论中的行为,这是意料之中的,正如其他人所说,这也是合乎逻辑的:

    • 过早的优化是万恶之源,线程上下文切换代价高昂,因此运行时不容易进行这些操作
    • 这个 test 函数并不完全在并发上下文中,因为代码在 MainActor 和您的类,因此,Swift运行时不知道它必须返回到协作线程池。

    如果你将你的类转换为演员,你会看到你期望的行为。以下是根据问题中的代码调整的演员:

    actor ThreadTester {
        func viewDidLoad() {
            Task.detached(priority: .userInitiated) {
                await self.test()
            }
        }
    
        @MainActor func getBounds() async -> CGRect {
            .zero
        }
    
        func test() async {
            print("test 1", Thread.isMainThread) // false
            let bounds = await self.getBounds()
            print("test 2", Thread.isMainThread) // true
        }
    }
    
    Task {
        await ThreadTester().viewDidLoad()
    }
    

    您可以在 actor class ,不影响其他代码,您将始终看到这两种行为。

    如果参与并发操作的所有实体都已经是结构化并发家族的一部分,那么Swift的结构化并发效果最好,因为在这种情况下,编译器拥有做出明智决策所需的所有信息。

        5
  •  1
  •   Soumya Mahunt    3 年前

    检查哪个线程任务正在运行是不可靠的,因为快速并发可能会在多个任务中使用相同的线程来避免上下文切换。您必须使用actor隔离来确保您的任务不会在actor上执行(这适用于任何actor以及 MainActor ).

    首先,swift中的演员是可重新进入的。这意味着无论何时 async callactor挂起当前任务,直到方法返回并继续执行提交的其他任务。这样可以确保actor永远不会因为长时间运行的任务而被阻止。所以,如果你打电话给任何人 异步 呼叫内部 test 方法,并担心该方法将在主线程上执行,那么您就不用担心了。由于您的ViewController类将是 主要参与者 孤立的代码变成:

    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            await self.test()
        }
    }
    
    func getBounds() -> CGRect {
        let bounds = self.view.bounds
        return bounds
    }
    
    func test() async {
        // long running async calls
        let bounds = self.getBounds()
        // long running async calls
    }
    

    现在,如果您的一些长时间运行的调用是同步的,那么您必须删除 测验 方法来自 主要参与者 的隔离通过应用 nonisolated 属性此外,使用创建任务 Task.init 继承当前actor上下文,然后在actor上执行该上下文,为了防止这种情况发生,您必须使用 Task.detached 执行 测验 方法

    override func viewDidLoad() {
        super.viewDidLoad()
        Task.detached {
            await self.test()
        }
    }
    
    func getBounds() -> CGRect {
        let bounds = self.view.bounds
        return bounds
    }
    
    nonisolated func test() async {
        // long running async calls
        // long running sync calls
        let bounds = await self.getBounds()
        // long running async calls
        // long running sync calls
    }
    
        6
  •  0
  •   M Wilm    4 年前

    我对一个运行长任务并且应该始终在后台线程上运行的函数也有同样的问题。最终,使用新的Swift异步等待语法,我找不到一种基于Task或TaskGroup的简单方法来确保这一点。 Task.detached调用将使用不同的线程,Task调用本身也是如此。如果这是从主线程调用的,它将是另一个线程,如果不是,它可以是主线程。 最终,我找到了一个始终有效的解决方案,但看起来非常“粗糙”。

    1. 确保您的后台函数不是与主线程隔离的,因为它是MainActor隔离类(如视图控制器)的一部分

      nonisolated func iAllwaysRunOnBackground() {}
      
    2. 测试主线程,如果在主线程上执行,则在Task中再次调用函数,等待执行并返回

      nonisolated func iAllwaysRunOnBackground() async throws {
        if Thread.isMainThread {
           async let newTask = Task {
             try await self.iAllwaysRunOnBackground()
           }
          let _ = try await newTask.value
          return 
        }
      
        function body
      }