New task-creation APIs

In Swift 6 all task-creation APIs have been updated to use SE-0430 and SE-0431. This means from what I can see, the closure passed to task-creation APIs now are not required to be @Sendable anymore but are sending and @isolation(any).

My interest in these proposals came from this section where it is specifically mentioned, that functions that wrap the creation of Tasks are recommended to switch to @isolation(any).

Now, I've read through both proposals, however I found it hard to understand the concrete difference between the two mentioned Task creation methods. This is a snippet of the code I used to try out the differences:

func newSend(
    operation: sending @escaping @isolated(any) () async -> Void
) {
    Task {
        await operation()
    }
}

func oldSend(
    operation: @escaping @Sendable () async -> Void
) {
    Task {
        await operation()
    }
}

Would someone help me understand compile-time and run-time differences of these two approaches? Or at least point me to resources where this might be documented.

Thank you.

The direct implication of the change is @isolated(any) behaviour described in the proposal:

A function value with this type dynamically carries the isolation of the function that was used to initialize it.

Plus runtime behaviour described here. Note that such closure now carries isolation parameter within it which you can access as well by operation.isolation.

It also states direct effect on Task initializers:

These APIs now all synchronously enqueue the new task directly on the appropriate executor for the task function's dynamic isolation.

There was no mechanism to express such dynamic isolation in the language before.


For @Sendable vs sending, the latter is just more relaxed constraint, allowing closures in disconnected region to be passed, while they might not be @Sendable.

@vns
Synchronous enqueueing, got it.

Would you mind sharing an example how @Sendable vs sending affects the snippets I posted? What exactly is more relaxed?

It is able to reason within region-based isolation what is safe to pass, extending options. The simplest case:

func oldSendable(_ body: @escaping @Sendable () async -> Void) {
    Task {
        await body()
    }
}

func newSending(_ body: sending @escaping () async -> Void) {
    Task {
        await body()
    }
}

func test() {
    // this one is clearly safe to pass
    let nsc: () async -> Void = {
    }
    oldSendable {
        await nsc()  // error
    }
    newSending {
        await nsc()  // ok
    }
}
1 Like

@vns Ah, region-based isolation, got it as well.

I just tried the same API with TaskGroup instead of Task:

func newSend(
    operation: sending @escaping @isolated(any) () async -> Void
) async {
    await withTaskGroup(of: Void.self) { taskGroup in
        taskGroup.addTask { // error: Task-isolated value of type '() async -> Void' passed as a strongly transferred parameter; later accesses could race
            await operation()
        }
    }
}

func oldSend(
    operation: @escaping @Sendable () async -> Void
) async {
    await withTaskGroup(of: Void.self) { taskGroup in
        taskGroup.addTask {
            await operation()
        }
    }
}

This does not work, somehow.

This compiler message was mentioned here Feedback wanted: confusing concurrency diagnostic messages - #12 by twof as being unclear, but it does not seem to be clearer by now...

There are few similar issues already with closures, so that seems to be a bug:

Reported this here: Wrong error when passing `sending` closure to `TaskGroup` · Issue #76242 · swiftlang/swift · GitHub, for anyone following up.

1 Like