Open3

Swift: async letの暗黙的await

kabeyakabeya

Swift Concurrencyで、async letで受けた変数をawaitしない場合、スコープから抜けるときに暗黙的にキャンセル&awaitする、という話があります。

https://stackoverflow.com/questions/76882273/async-let-fire-and-forget

https://github.com/apple/swift-evolution/blob/main/proposals/0317-async-let.md#implicit-async-let-awaiting

https://zenn.dev/koher/articles/swift-concurrency-cheatsheet#after-7

本当か?今そうなってないんじゃないか?ということで調べました。

func funcA() async {
    NSLog("funcA start")
    sleep(2)
    NSLog("funcA %@", Task.isCancelled ? "cancelled" : "not cancelled")
    NSLog("funcA finish")
}

func funcB() async {
    NSLog("funcB start")
    sleep(1)
    NSLog("funcB %@", Task.isCancelled ? "cancelled" : "not cancelled")
    NSLog("funcB finish")
}

func funcC() async {
    NSLog("funcC start")
    sleep(3)
    NSLog("funcC %@", Task.isCancelled ? "cancelled" : "not cancelled")
    NSLog("funcC finish")
}

func funcX() async {
    NSLog("funcX start")
    async let _ = funcA()
    async let _ = funcB()
    async let _ = funcC()
}

for i in (1 ... 3) {
    NSLog("\(i)")
    await funcX()
    NSLog("funcX finish")
}

funcXasync letfuncA,funcB,funcCを呼び出しますが、awaitしません。
見た目上は、待たずに放り投げています。
funcAは2秒、funcBは1秒、funcCは3秒かかります。

さて、出力結果は以下のようになりました。

1
funcX start
funcA start
funcB start
funcC start
funcB not cancelled
funcB finish
funcA not cancelled
funcA finish
funcC cancelled
funcC finish
funcX finish
2
funcX start
funcA start
funcB start
funcC start
funcB not cancelled
funcB finish
funcA not cancelled
funcA finish
funcC cancelled
funcC finish
funcX finish
3
funcX start
funcA start
funcB start
funcC start
funcB not cancelled
funcB finish
funcA not cancelled
funcA finish
funcC cancelled
funcC finish
funcX finish

待ってますね…

要約すると、

  • 呼び出し元はawaitしなくても、すべての呼び出しが返ってくるのを待ちます。
  • このため呼び出し元はすべての呼び出しが返ってくるのにかかる時間だけ実行時間がかかります。
  • 同時に呼び出されている非同期処理のうち、一番長いものだけがキャンセルされます(?)

この最後の動作がよく分かりませんね…

kabeyakabeya

別のケースもいくつかテストしました。

func funcSleep(_ name: String, _ seconds: UInt32) async {
    NSLog("%@ start", name)
    sleep(seconds)
    NSLog("%@ %@", name, Task.isCancelled ? "cancelled" : "not cancelled")
    NSLog("%@ finish", name)
}

func funcX() async {
    NSLog("funcX start")
    async let _ = funcSleep("sleep2", 2)
    async let _ = funcSleep("sleep1", 1)
    async let _ = funcSleep("sleep3", 3)
}

func funcY() async {
    NSLog("funcY start")
    async let _ = funcSleep("sleep2", 2)
    async let r: () = funcSleep("sleep1", 1)
    async let _ = funcSleep("sleep3", 3)
    await r
}

for i in (1 ... 3) {
    NSLog("\(i)")
    await funcX()
    NSLog("funcX finish")
}

print("")

for i in (4 ... 6) {
    NSLog("\(i)")
    async let _ = funcX()
    NSLog("funcX finish")
}

print("")

for i in (7 ... 9) {
    NSLog("\(i)")
    await funcY()
    NSLog("funcY finish")
}

print("")

for i in (10 ... 12) {
    NSLog("\(i)")
    async let _ = funcY()
    NSLog("funcY finish")
}

結果です。

1
funcX start
sleep2 start
sleep3 start
sleep1 start
sleep1 not cancelled
sleep1 finish
sleep2 not cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish
funcX finish
2
funcX start
sleep2 start
sleep1 start
sleep3 start
sleep1 not cancelled
sleep1 finish
sleep2 not cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish
funcX finish
3
funcX start
sleep2 start
sleep1 start
sleep3 start
sleep1 not cancelled
sleep1 finish
sleep2 not cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish
funcX finish

4
funcX finish
funcX start
sleep2 start
sleep1 start
sleep3 start
sleep1 cancelled
sleep1 finish
sleep2 cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish
5
funcX finish
funcX start
sleep2 start
sleep1 start
sleep3 start
sleep1 cancelled
sleep1 finish
sleep2 cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish
6
funcX finish
funcX start
sleep2 start
sleep1 start
sleep3 start
sleep1 cancelled
sleep1 finish
sleep2 cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish

7
funcY start
sleep2 start
sleep1 start
sleep3 start
sleep1 not cancelled
sleep1 finish
sleep2 not cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish
funcY finish
8
funcY start
sleep2 start
sleep1 start
sleep3 start
sleep1 not cancelled
sleep1 finish
sleep2 not cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish
funcY finish
9
funcY start
sleep2 start
sleep1 start
sleep3 start
sleep1 not cancelled
sleep1 finish
sleep2 not cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish
funcY finish

10
funcY finish
funcY start
sleep2 start
sleep1 start
sleep3 start
sleep1 cancelled
sleep1 finish
sleep2 cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish
11
funcY finish
funcY start
sleep2 start
sleep1 start
sleep3 start
sleep1 cancelled
sleep1 finish
sleep2 cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish
12
funcY finish
funcY start
sleep2 start
sleep1 start
sleep3 start
sleep1 cancelled
sleep1 finish
sleep2 cancelled
sleep2 finish
sleep3 cancelled
sleep3 finish
  • 呼び出し元自体がawaitされていなくてasync let放り投げされていると、呼び出し先はすべてキャンセルされます。
  • 呼び出し元自体がawaitされている場合、呼び出し先の一番短い処理をasync letawaitしても、一番長い処理がキャンセルされ、それ以外はキャンセルされないというのは変わりません。
kabeyakabeya

なんかもうちょっと体系的に整理したいですね。思いついたのを適当に動かしてみるだけでなく…