🧵

クロージャのキャプチャリストの処理はクロージャ生成時に生成元のスレッドで実行されることがわかりやすく表現できるサンプル

2023/07/20に公開

概要

ロージャのキャプチャリストの代入は、クロージャの生成時に生成元のスレッドで実行されることがわかりやすく表現できるサンプルできたので残しておきます。

試した環境

  • Xcode 14.3.1
  • Playground

サンプル

同期処理

まずは同期処理で考えた方がシンプル。

struct Piyo {
    static func make() -> Piyo {
        print("Piyo:", #function, "isMainThread?", Thread.isMainThread)
        return .init()
    }
}

// MARK: -

print("begin isMainThread?", Thread.isMainThread)

let closure = { [piyo = Piyo.make()] in
    print("closure:", piyo, "isMainThread?", Thread.isMainThread)
}

print("closure()")
closure()

print("end")

結果は次のとおり

begin isMainThread? true
Piyo: make() isMainThread? true
closure()
closure: Piyo() isMainThread? true
end

closure()関数の実行前、
let closureを作成時にPiyoのmakeメソッドが実行されているのがわかる。

すべてメインスレッドで実行しているのもisMainThread()関数で念のため確認。

非同期処理

同期処理のクロージャの例がわかりやすいが、身近な話に近づけていくためにクロージャを即時実行される非同期処理に置き換えてみる。

そうなるとついでにweakにも触れる。

class Hoge {
    static func make() -> Hoge {
        print("Hoge:", #function, "isMainThread?", Thread.isMainThread)
        return .init()
    }

    deinit {
        print("Hoge:", #function)
    }
}

struct Piyo {
    static func make() -> Piyo {
        print("Piyo:", #function, "isMainThread?", Thread.isMainThread)
        return .init()
    }
}

print("begin isMainThread?", Thread.isMainThread)

// クロージャの作成時にキャプチャリストの処理が実行されるはず
DispatchQueue.global().async { [weak hoge = Hoge.make(), piyo = Piyo.make()] in
    // クロージャ作成時に生成されたhogeはweakとなり、
    // 強参照の参照カウントが上がらないため、
    // クロージャを実行時にはすでに解放されている。
    print("closure:", hoge, piyo, "isMainThread?", Thread.isMainThread)
    // さらにhoge, piyoインスタンスはクロージャ生成したメインスレッドで生成されたが、
    // このクロージャの別スレッドで利用されることになりスレッドを越えている。
}

print("end")

出力結果は次のとおり

begin isMainThread? true
Hoge: make() isMainThread? true
Hoge: deinit
Piyo: make() isMainThread? true
end
closure: nil Piyo() isMainThread? false

言いたいことが2つになってしまったが

  • Hogeクラスのインスタンスはweakによって参照カウントが上げられていないので即時deinit
    • そのタイミングはクロージャの作成時だとわかる(同期処理の例の方がわかりやすいけど)
  • DispatchQueue.global().asyncのクロージャはメインスレッドでははない
    • Hogeのmakeはクロージャ生成がメインスレッドなのでメインスレッドでクロージャ作成時に実行される

weakの話はとりあえずわかっていると思うのでそっとしておいて、ここで伝えたいのはキャプチャリストに入れようが外部から作成したインスタンスを非同期処理のクロージャに入れるということは、スレッドをまたがってしまうということだ。

言い換えると、キャプチャリストに入れても境界を越えてくるので、いい感じにSendable化しないといけない。

Task

今更DispatchQueueを使いたいわけじゃないので、Taskの例を示しておく。

print("begin isMainThread?", Thread.isMainThread)

Task { @MainActor in
    print("MainActor:")
    Task.detached { [weak hoge = Hoge.make(), piyo = Piyo.make()] in
        print("closure:", hoge, piyo, "isMainThread?", Thread.isMainThread)
    }
}

print("end")

結果は次のとおり

begin isMainThread? true
end
MainActor:
Hoge: make() isMainThread? true
Hoge: deinit
Piyo: make() isMainThread? true
closure: nil Piyo() isMainThread? false

MainActorを即時実行できていないことから、PlaygroundのTop-Levelの実行はMainActorではないがメインスレッドなんだろうか、という謎の余韻を残してしまってはいる。

Discussion