【问题标题】:Need help to understand the order of execution in conjunction with async and await需要帮助来理解 async 和 await 的执行顺序
【发布时间】:2022-02-05 11:38:39
【问题描述】:

通过下面的示例代码,我进行了测试以了解 Swift 中的 async 和 await 机制。实现了过程的顺序。我到控制台的测试打印确实以预期的顺序出现(步骤 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
    }
}

【问题讨论】:

    标签: swift async-await


    【解决方案1】:

    你问:

    那么为什么 Message1 在第 1 步之后没有按编码出现?

    因为您使用 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 Concurrency 独有的。如果您也试图阻止主线程等待 GCD 后台队列上缓慢运行的东西,这个问题就会显现出来。


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

    回想一下,使用 Swift,该语言允许我们维护运行时契约,线程将始终能够向前推进。正是基于这个合约,我们构建了一个协作线程池作为 Swift 的默认执行器。当您采用 Swift 并发时,重要的是要确保您继续在代码中维护此合约,以便协作线程池能够以最佳方式运行。

    因此,您不应在 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 所说,这“不会阻塞底层线程”(强调)。


    另外,请注意,此代码不是线程安全的。您正在从多个线程访问testasyncDone,而没有任何同步。打开Thread Sanitizer(TSAN),它会报告:

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


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

    让我们考虑一堆替代方案。

    1. 如果您确实需要并行运行两个任务并将其与某个状态变量协调,一个方法计数直到另一个方法更改该变量的状态,您可以首先创建一个actor 来捕获此状态:

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

      然后你可以检查这个演员的状态:

      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"
              }
          }
      }
      

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

    【讨论】:

      猜你喜欢
      • 2021-07-25
      • 2022-08-14
      • 1970-01-01
      • 2015-09-16
      • 2018-12-15
      • 2018-03-20
      • 2020-02-11
      • 2011-05-04
      • 1970-01-01
      相关资源
      最近更新 更多