你问:
那么为什么 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。
所以,我怀疑这会触发响应,“好吧,如果我不能阻塞主线程,那我该怎么办?”
让我们考虑一堆替代方案。
-
如果您确实需要并行运行两个任务并将其与某个状态变量协调,一个方法计数直到另一个方法更改该变量的状态,您可以首先创建一个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" }
}
}
-
或者,您也可以完全绕过这个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()
}
}
-
或者,如果您只是想在 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" }
}
}
-
或者,如果你想在主线程上设置一个计时器,并且你想在异步任务完成时取消它:
@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 任务结束时启动主线程上需要发生的任何事情。