需要帮助来理解与 async 和 await 结合的执行顺序

Need help to understand the order of execution in conjunction with async and await

我通过下面的示例代码进行了测试,以了解 Swift 中的异步和等待机制。实现了过程的顺序。我向控制台的测试打印确实按预期顺序出现(步骤 1-4)。然而,令我困惑的是,应出现在 UI 上的测试消息并未按预期顺序出现。这两个消息(Message1Message2)在第 4 步之后的整个过程结束时一起出现。那么为什么 Message1 没有像编码那样在第 1 步之后立即出现?

import UIKit

class ViewController: UIViewController {
    
    var testasyncDone = false
    @IBOutlet weak var MyButton2: UIButton!
    @IBOutlet weak var Message1: UITextField!
    @IBOutlet weak var Message2: UITextField!
    
    @IBAction func MyButton2pressed(_ sender: UIButton) {
        print("MyButton Pressed step 1")
        
        // This first message shall appear right after the button
        // is pressed
        Message1.text =  "In Button action - start"
        
        // The async task is defined and started
        testasyncDone = false
        Task.detached {
            await self.testasync()
            print("MyButton Pressed step 4")
        }
        
        // The intention of the next lines is to hold the
        // processing of the main thread until the async task is
        // completed.
        var count = 0
        repeat {
            count = count + 1
            usleep(100000)
            print (count)
        } while testasyncDone == false && count < 100
        
        // After the async task is done, the second message shall show up
        Message2.text = "In Button action - end"
    }
    
    func testasync() async {
        print("in testasync step 2")
        sleep(2)
        print("in testasync step 3")
        testasyncDone = true
    }
}

你问:

So why does Message1 not appear right after step 1 as coded?

因为您用 repeat-while 循环阻塞了主线程。您的代码完美地展示了为什么您应该 永远不会 阻塞主线程,因为 UI 在您释放主线程之前无法更新。任何系统事件也将被阻止。如果你阻塞主线程足够长的时间,你甚至有可能让你的应用程序被 watchdog process(用终止代码 0x8badf00d 指定,读作“吃坏食物”)毫不客气地杀死。

您的代码表示:

// The intention of the next lines is to hold the
// processing of the main thread until the async task is
// completed.

的问题所在。这就是您的 UI 冻结的原因。这是绝对要避免的。

仅供参考,此行为并非 Swift 并发所独有。如果您试图阻止主线程在 GCD 后台队列上缓慢地等待某事 运行,那么这个问题就会显现出来。


顺便说一句,我注意到 testAsync 正在调用 sleep。但是,正如 Apple 在 Swift concurrency: Behind the scenes 中所说:

Recall that with Swift, the language allows us to uphold a runtime contract that threads will always be able to make forward progress. It is based on this contract that we have built a cooperative thread pool to be the default executor for Swift. As you adopt Swift concurrency, it is important to ensure that you continue to maintain this contract in your code as well so that the cooperative thread pool can function optimally.

因此你不应该在 testAsync 中调用 sleep。您可以使用 Task.sleep(nanoseconds:),但:

func testAsync() async throws {
    print("in testAsync step 2")
    try await Task.sleep(nanoseconds: NSEC_PER_SEC * 2)
    print("in testAsync step 3")
    testAsyncDone = true
}

它看起来像传统的“休眠”API,但正如 the docs 所说,这“不会 阻塞底层线程”(强调已添加).


另请注意,此代码不是 thread-safe。您在没有任何同步的情况下从多个线程访问 testasyncDone。打开Thread Sanitizer(TSAN),会报:

您可以自己同步(锁或 GCD 是传统机制),或者使用新的 Swift 并发系统,我们将使用 actor。请参阅 WWDC 2021 视频,Protect mutable state with Swift actors


所以,我怀疑这会触发响应,“好吧,如果我不能阻塞主线程,那我该怎么办?”

让我们考虑一堆备选方案。

  1. 如果你真的需要并行执行两个任务 运行 并将其与某个状态变量协调,一个方法计数直到另一个改变该变量的状态,你可以首先创建一个 actor 捕捉这个状态:

    actor TestAsyncState {
        private var _isDone = false
    
        func finish() {
            _isDone = true
        }
    
        func isDone() -> Bool {
            _isDone
        }
    }
    

    然后你可以检查这个actor状态:

    var testAsyncState = TestAsyncState()
    
    @IBAction func didTapButton(_ sender: UIButton) {
        print("MyButton Pressed step 1")
    
        Task.detached { [self] in
            await MainActor.run { message1.text = "In Button action - start" }
            try await self.testAsync()
            print("MyButton Pressed step 4")
            await testAsyncState.finish()
        }
    
        Task.detached { [self] in
            // The intention of the next lines is to keep ticking
            // until the state actor isDone or we reach 100 iterations
    
            var count = 0
            repeat {
                count += 1
                try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10)
                print(count)
            } while await !testAsyncState.isDone() && count < 100
    
            await MainActor.run { message2.text = "In Button action - finished" }
        }
    }
    
  2. 或者,您可以完全绕过这个 actor 状态变量,并在另一个完成时取消计数任务:

    @IBAction func didTapButton(_ sender: UIButton) {
        print("MyButton Pressed step 1")
    
        let tickingTask = Task.detached { [self] in
            // The intention of the next lines is to keep ticking
            // until this is canceled or we reach 100 iterations
    
            do {
                var count = 0
                repeat {
                    count += 1
                    try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10)
                    print(count)
                } while !Task.isCancelled && count < 100
    
                await MainActor.run { message2.text = "In Button action - finished" }
            } catch {
                await MainActor.run { message2.text = "In Button action - canceled" }
            }
        }
    
        Task.detached { [self] in
            await MainActor.run { message1.text =  "In Button action - start" }
            try await self.testAsync()
            print("MyButton Pressed step 4")
            tickingTask.cancel()
        }
    }
    
  3. 或者,如果您只想在 async 方法完成后在主线程上做一些事情,只需将它放在您正在等待的方法之后:

    @IBAction func didTapButton(_ sender: UIButton) {
        print("MyButton Pressed step 1")
    
        Task.detached { [self] in
            await MainActor.run { message1.text =  "In Button action - start" }
            try await self.testAsync()
            print("MyButton Pressed step 4")
    
            // put whatever you want on the main actor here, e.g.
    
            await MainActor.run { message2.text = "In Button action - finished" }
        }
    }
    
  4. 或者,如果你想在主线程上有一个滴答计时器并且你想在异步任务完成时取消它:

    @IBAction func didTapButton(_ sender: UIButton) {
        print("MyButton Pressed step 1")
    
        var count = 0
    
        let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
            count += 1
            print(count)
        }
    
        Task.detached { [self] in
            await MainActor.run { message1.text =  "In Button action - start" }
            try await self.testAsync()
            print("MyButton Pressed step 4")
    
            // put whatever you want on the main actor here, e.g.
    
            await MainActor.run {
                timer.invalidate()
                message2.text = "In Button action - finished"
            }
        }
    }
    

给猫剥皮的方法有很多种。但是,关键是 none 阻塞了主线程(或任何线程,就此而言),但是我们可以在 async 任务结束时启动主线程上需要发生的任何事情.