👏
Swift 6.2でのasync/awaitの挙動を見る
プチ解説
📍Swift6.2より、下記のproposalで「大抵の場合はメインスレッドで動作すれば嬉しいことが多い」という動機で、デフォルトのアクターがMainになりました(コンパイラフラグで変更できます)
📍Swift6.2より、下記のproposalで「nonisolatedな同期関数は実行元スレッドで動作するのに、nonisolatedな非同期関数だけ別スレッドで動作するのは理解が難しい」という理由で、nonisolated(nonsending)をデフォルト挙動とし両方とも呼び出し元のスレッドで動作する挙動に変わりましたが、upcoming feature flagでオンにしないと有効になりません。
概要
下記のサンプルコードを使いながら、新機能フラグがオンのとき、オフのときの実行スレッドを見ていきます。
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello World")
.task {
print("task: \(Thread.unsafeCurrent)")
let myObject = MyObject()
myObject.run()
await myObject.runAsync()
await myObject.runAsyncConcurrent()
myObject.runNonisolated()
await myObject.runNonisolatedAync()
await myObject.runNonisolatedAyncConcurrent()
}
}
}
class MyObject {
func run() {
print("\(#function): \(Thread.unsafeCurrent)")
}
func runAsync() async {
print("\(#function): \(Thread.unsafeCurrent)")
}
@concurrent func runAsyncConcurrent() async {
print("\(#function): \(Thread.unsafeCurrent)")
}
nonisolated func runNonisolated() {
print("\(#function): \(Thread.unsafeCurrent)")
}
nonisolated func runNonisolatedAync() async {
print("\(#function): \(Thread.unsafeCurrent)")
}
@concurrent nonisolated func runNonisolatedAyncConcurrent() async {
print("\(#function): \(Thread.unsafeCurrent)")
}
}
extension Thread {
nonisolated(unsafe) static var unsafeCurrent: Thread {
Self.current
}
}
nonisoated(nonsending) by Default = NO
の挙動
-
run()
とrunAsync()
はMyObject
がMainActorなので、そのままメインスレッドに隔離されて実行されています。 -
runAsyncConcurrent()
は隔離されていますが、@concurrent
を付けているので別スレッドで実行されています。 -
runNonisolated()
は隔離されておらず、実行元のスレッド(NSMainThread)で実行されています。 -
runNonisolatedAsync()
が、実行元の番号1のNSMainThreadではなく、番号3のNSThreadで実行されています。
task: <_NSMainThread: 0xb4f030000>{number = 1, name = main}
run(): <_NSMainThread: 0xb4f030000>{number = 1, name = main}
runAsync(): <_NSMainThread: 0xb4f030000>{number = 1, name = main}
runAsyncConcurrent(): <NSThread: 0xb4f397900>{number = 4, name = (null)}
runNonisolated(): <_NSMainThread: 0xb4f030000>{number = 1, name = main}
runNonisolatedAync(): <NSThread: 0xb4f2c3700>{number = 3, name = (null)}
runNonisolatedAyncConcurrent(): <NSThread: 0xb4f2c3700>{number = 3, name = (null)}
nonisoated(nonsending) by Default = YES
時の挙動
-
run()
とrunAsync()
はMyObject
がMainActorなので、そのままメインスレッドに隔離されて実行されています。 -
runAsyncConcurrent()
は隔離されていますが、@concurrent
を付けているので別スレッドで実行されています。 -
runNonisolated()
は隔離されておらず、実行元のスレッド(NSMainThread)で実行されています。 -
runNonisolatedAsync()
が、実行元の番号1のNSMainThreadで、そのまま実行されています。 -
runNonisolatedAyncConcurrent()
は、@concurrent
をつけているので別スレッド(NSThread, 4)で実行されています。
task: <_NSMainThread: 0x796808000>{number = 1, name = main}
run(): <_NSMainThread: 0x796808000>{number = 1, name = main}
runAsync(): <_NSMainThread: 0x796808000>{number = 1, name = main}
runAsyncConcurrent(): <NSThread: 0x7961a2880>{number = 4, name = (null)}
runNonisolated(): <_NSMainThread: 0x796808000>{number = 1, name = main}
runNonisolatedAync(): <_NSMainThread: 0x796808000>{number = 1, name = main}
runNonisolatedAyncConcurrent(): <NSThread: 0x7961a2880>{number = 4, name = (null)}
s
まとめ
- デフォルトでMainActorに隔離されるので、隔離されたメソッドは同期/非同期関わらず、原則自身のアクターで実行されます(例外は@concurrentを明示的につけた場合)
-
nonisoated(nonsending) by Default
フラグが NO の場合、非隔離の同期メソッドは呼び出し元のアクターで動作しますが、非隔離の非同期メソッドは別スレッドに移動します。 -
nonisoated(nonsending) by Default
フラグが YES の場合、非隔離のメソッドは同期/非同期関わらず、原則呼び出し元アクターで動作します。(例外は@concurrentを明示的につけた場合)
確かに整理してみると、isolatedなメソッドは全てそのアクター内で実行され、nonisolatedなメソッドは全て呼び出し元のアクターで実行されるというのは、すごく対称性があって分かりやすいような気もします。
(Swiftむずくね?)
Discussion