我正在尝试学习快速并发,但它带来了很多混乱。我知道 Task {} 是一个异步单元,它允许我们从同步上下文桥接异步函数调用。它类似于 DispatchQueue.Global() ,后者将在某个任意线程上执行该块。
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
let data = try await asychronousApiCall()
print(data)
} catch {
print("Request failed with error: \(error)")
}
}
for i in 1...30000 {
print("Thread \(Thread.current)")
}
}
我的 asychronousApiCall 函数如下
func asychronousApiCall() async throws -> Data {
print("starting with asychronousApiCall")
print("Thread \(Thread.current)")
let url = URL(string: "https://www.stackoverflow.com")!
// Use the async variant of URLSession to fetch data
// Code might suspend here
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
当我尝试这个实现时。我总是看到“starting with asychronousApiCall”是在
for loop
完成并且线程是 MainThread 之后打印的。
像这样
Thread <_NSMainThread: 0x600000f10500>{number = 1, name = main}
tl;博士
来自 GCD 和
Thread
编程,我们过去经常关注诸如线程代码运行的问题。在 Swift 并发中,我们关注参与者而不是线程,并让编译器和运行时处理其余的事情。因此,虽然了解 Swift 并发线程模型的工作原理很有趣,但在实践中,我们重点关注参与者。
你说:
我知道
是一个异步单元,允许我们从同步上下文桥接Task {}
函数调用。async
是的。
您继续:
它类似于
,后者将在某个任意线程上执行该块。DispatchQueue.global()
不,如果你从主角那里称呼它,它更类似于
DispatchQueue.main.async {…}
。正如文档所说,它“代表当前参与者异步执行给定的非抛出操作,作为新的顶级任务的一部分”[强调]。即,如果您当前是主要演员,则任务也将代表主要演员运行。
虽然专注于直接 GCD 到并发映射可能是错误的,但
Task.detached {…}
与 DispatchQueue.global().async {…}
更具可比性。
您评论:
打印是其他线程。Thread
在该屏幕快照中,它们显示在挂起点之前(即,在
await
之前)它位于主线程上(这是有道理的,因为它代表同一个参与者运行它)。但他们也强调,在挂起点之后,它在另一个线程上(这可能看起来违反直觉,但这是挂起点之后可能发生的情况)。
作者所表现出的精确行为(在挂起点之后与主要参与者隔离的代码可以在不同的线程上运行)仅在延续不执行任何需要主要参与者的操作时发生,并且在当代版本的编译器上不可重现.
话虽如此,作者更广泛的观点仍然成立,即,一般来说,延续中的代码可能在与挂起点之前不同的线程上运行。但是,在那篇文章中的特定示例中,他展示了主要演员的这种行为,这可能是旧版本编译器的特殊行为/优化。我经历了作者在较旧的编译器中发现的相同行为,但不再是了。
以下内容可能更好地说明了延续可能在与您预期不同的线程上运行的事实:
nonisolated func threadInfo(context: String, message: String? = nil) {
print(context, Thread.current, message ?? "")
}
nonisolated func spin(for duration: Duration) throws {
let start = ContinuousClock.now
while start.duration(to: .now) < duration {
try Task.checkCancellation()
}
}
actor Foo {
func foo() async throws {
threadInfo(context: #function, message: "foo’s starting thread")
async let value = Bar().bar()
try spin(for: .seconds(0.5))
threadInfo(context: #function, message: "foo still on starting thread")
let result = try await value
threadInfo(context: #function, message: "foo’s continuation often runs on the thread previously used by bar! result=\(result)")
}
}
actor Bar {
func bar() throws -> Int {
threadInfo(context: #function, message: "bar is running on some other thread")
try spin(for: .seconds(1))
return 42
}
}
制作:
foo() <NSThread: 0x6000017612c0>{number = 7, name = (null)} foo’s starting thread
bar() <NSThread: 0x600001760540>{number = 6, name = (null)} bar is running on some other thread
foo() <NSThread: 0x6000017612c0>{number = 7, name = (null)} foo still on starting thread
foo() <NSThread: 0x600001760540>{number = 6, name = (null)} foo’s continuation often runs on the thread previously used by bar! result=42
请注意,这种精确的行为可能会发生变化,具体取决于运行时正在执行的其他操作和/或未来编译器可能引入的任何优化。 “带回家”的消息只是一个人应该意识到,延续可能会在与挂起点之前的代码不同的线程上运行(在那些极其罕见的情况下,您可能会调用一些具有一些特定于线程的假设的 API……一般来说,我们只需不关心延续运行哪个线程)。
FWIW,在上面的示例中,您只检查挂起点之前的线程,而不是之后。图 8 的目的是说明挂起点之后使用的线程可能与挂起点之前使用的线程不同(尽管他们对主要参与者的这种行为的说明并不是一个很好的例子)。
如果您有兴趣了解有关其中一些实现细节的更多信息,我可能建议观看 WWDC 2021 视频Swift 并发:幕后。该视频讨论了 Swift 并发的这一特性。
虽然看起来很有趣
Thread.current
,但应该指出的是,苹果正在试图让我们摆脱这种做法。例如,在 Swift 5.7 中,如果我们从异步上下文中查看 Thread.current
,我们会收到警告:
类属性“current”在异步上下文中不可用; Thread.current 不能在异步上下文中使用。这是 Swift 6 中的错误
Swift 并发的整体思想是,我们不再考虑线程,而是让 Swift 并发代表我们选择适当的线程(这巧妙地避免了代价高昂的上下文切换;有时会导致代码在线程以外的线程上运行)否则我们可能会期望)。