🙌

【Swift Concurrency】sendingクロージャについての軽く調べてみた

2024/10/14に公開

調査:sendingクロージャについて

sendingクロージャは、クロージャ内で参照しているnon-Sendableな値をsendingされたものとして扱うクロージャ。

より詳しく言うと、「クロージャがnon-Sendableな値をキャプチャした時に、その値がキャプチャ後に使用されないこと」を保証させる。

個人的にsendingクロージャは、@Sendableクロージャが安全にnon-Sendableな値をキャプチャできるようになったものだと認識。

@Sendableクロージャは、クロージャ内でnon-Sendableな値をキャプチャすることができない。

class NonSendableClass {
    var num = 0
}

func action1(sendableClosure: @Sendable () -> Void) {
    // 何らかの処理
}

func action2() {
    let nonSendableClass = NonSendableClass()

    action1(sendableClosure: {
        print(nonSendableClass) // ※1
    })
}

// ※1: Capture of 'nonSendableClass' with non-sendable type 'NonSendableClass' in a `@Sendable` closure

上記の例では、non-SendableClassをキャプチャしても特に不具合が起こることはないが、@Sendableクロージャ内はnon-Sendableな値をキャプチャできないためエラーが出る。

action1関数を@Sendableクロージャからsendingクロージャに変更してみる。

class NonSendableClass {
    var num = 0
}

func action1(sendingClosure: sending () -> Void) {
    // 何らかの処理
}

func action2() {
    let nonSendableClass = NonSendableClass()

    action1(sendingClosure: {
        print(nonSendableClass)
    })
}

先ほどの@Sendableクロージャと違い、sendingクロージャではnon-Sendableな値をキャプチャしても問題はない。

おそらくではあるが、sendingクロージャ内でnon-Sendableな値をキャプチャした場合、その値はsendingしたものとみなされるためだと思われる。

検証として以下のコードのようにaction1関数の実行後に、non-SendableClassにアクセスするとエラーが出る。

class NonSendableClass {
    var num = 0
}

func action1(sendingClosure: sending () -> Void) {
    // 何らかの処理
}

func action2() {
    let nonSendableClass = NonSendableClass()

    action1(sendingClosure: { // ※1
        print(nonSendableClass)
    })

    print(nonSendableClass)
}

// ※1: Value of non-Sendable type '@noescape @callee_guaranteed () -> ()' accessed after being transferred; later accesses could race

sendingクロージャについての調査は以上になります。他に何か分かったことがあれば追記しようと思います。

追記:変数のキャプチャについて

sendingクロージャは変数のキャプチャ可能。

func action1(sendingClosure: sending () -> Void) {
    // 何らかの処理
}

func action2() {
    var num = 0

    action1(sendingClosure: {
        print(num)
    })

    num += 1
    print(num)
}

@Sendingクロージャは変数のキャプチャ不可。

func action1(sendableClosure: @Sendable () -> Void) {
    // 何らかの処理
}

func action2() {
    var num = 0

    action1(sendableClosure: {
        print(num) // ※1
    })

    num += 1
    print(num)
}

// ※1: Reference to captured var 'num' in concurrently-executing code

蛇足:Task関連のoperationについて

TaskのイニシャライザやTaskGroup等のaddTaskoperation@Sendableなクロージャからsendingなクロージャに変更されています。以下はTask.init(operation:)の変更前と変更後です。

[変更前]

@discardableResult
public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success)

[変更後]

@discardableResult
public init(priority: TaskPriority? = nil, operation: sending @escaping @isolated(any) () async -> Success)

Discussion