隔离(任何)
Isolated(any)

原始链接: https://nshipster.com/isolated-any/

## @isolated(any): 摘要 `@isolated(any)` 是 Swift 并发中一个看似矛盾的属性,常见但通常可以忽略。它被引入是为了解决在使用异步函数时信息丢失的问题——特别是无法静态确定函数的隔离性(它绑定到哪个 actor)。 它为函数类型添加了一个 `isolation` 属性,允许访问它是否隔离到 actor。这使得诸如更智能的任务调度等功能成为可能,并且至关重要的是,在使用 `Task` 时保留了顺序保证——允许同步提交到 actor。 虽然它*不影响*你作为开发者调用函数的方式,但它为 API 本身提供了有价值的信息。你可能不需要直接使用它,但在将隔离函数捕获并传递到利用它的 API 时,采用它是很有益的。 本质上,`@isolated(any)` 是一种幕后机制,可以改进并发系统,提供未来灵活性的潜力,并实现优化,而无需开发人员进行重大干预。

相关文章

原文

Ahh, @isolated(any). It’s an attribute of contradictions. You might see it a lot, but it’s ok to ignore it. You don’t need to use it, but I think it should be used more. It must always take an argument, but that argument cannot vary.

Confusing? Definitely. But we’ll get to it all.


To understand why @isolated(any) was introduced, we need to take a look at async functions.

let respondToEmergency: () async -> Void

This is about as simple a function type as we can get. But, things start to get a little more interesting when we look at how a function like this is used. A variable with this type must always be invoked with await.

await respondToEmergency()

This, of course, makes sense. All async functions must be called with await. But! Consider this:

let sendAmbulance: @MainActor () -> Void = {
    print("🚑 WEE-OOO WEE-OOO!")
}

let respondToEmergency: () async -> Void = sendAmbulance

await respondToEmergency()

The explicit types are there to help make what’s going on clear. We first define a synchronous function that must run on the MainActor. And then we assign that to a plain old, non-MainActor async function. We’ve changed so much that you might find it surprising this even compiles.

Remember what await actually does. It allows the current task to suspend. That doesn’t just let the task wait for future work to complete. It also is an opportunity to change isolation. This makes async functions very flexible!

Just like a dispatcher doesn’t sit there doing nothing while waiting for the ambulance to arrive, a suspended task doesn’t block its thread. When the dispatcher puts you on hold to coordinate with the ambulance team, that’s the isolation switch - they’re transferring your request to a different department that specializes in that type of work.

callAsFunction.

struct IsolatedAnyFunction<T> {
    let isolation: (any Actor)?
    let body: () async -> T

    func callAsFunction() async -> T {
        await body()
    }
}

let value = IsolatedAnyFunction(isolation: MainActor.shared, body: {
    // isolated work goes here
})

await value()

This analogy is certainly not perfect, but it’s close enough that it might help.

There is one other subtle change that @isolated(any) makes to a function that you should be aware of. Its whole purpose is to capture the isolation of a function. Since that could be anything, callsites need an opportunity to switch. And that means an @isolated(any) function must be called with an await — even if it isn’t itself explicitly async.

func dispatchResponder(_ responder: @isolated(any) () -> Void) async {
    await responder() // note the function is synchronous
}

This makes synchronous functions marked with @isolated(any) a little strange. They still must be called with await, yet they aren’t allowed to suspend internally?

As it turns out, there are some valid (if rare) situations where such an arrangement can make sense. But adding this kind of constraint to your API should at least merit some extra documentation.

the proposal:

This allows the API to make more intelligent scheduling decisions about the function.

I’ve highlighted “intelligent scheduling”, because this is the key component of @isolated(any). The attribute gives you access to the isolation of a function argument. But what would you use that for?

Did you know that, before Swift 6.0, the ordering of the following code was undefined?

@MainActor
func threeAlarmFire() {
    Task { print("🚒 Truck A reporting!") }
    Task { print("🚒 Truck B checking in!") }
    Task { print("🚒 Truck C on the case!") }
}

Ordering turns out to be a very tricky topic when working with unstructured tasks. And while it will always require care, Swift 6.0 did improve the situation. We now have some stronger guarantees about scheduling work on the MainActor, and @isolated(any) was needed to make that possible.

Take a look at this:

@MainActor
func sendAmbulance() {
    print("🚑 WEE-OOO WEE-OOO!")
}

nonisolated func dispatchResponders() {
    // synchronously enqueued
    Task { @MainActor in
        sendAmbulance()
    }

    // synchronously enqueued
    Task(operation: sendAmbulance)

    // not synchronously enqueued!
    Task {
        await sendAmbulance()
    }
}

These are three ways to achieve the same goal. But, there is a subtle difference in how the last form is scheduled. Task takes an @isolated(any) function so it can look at its isolation and synchronously submit it to an actor. This is how ordering can be preserved! But, it cannot do that in the last case. That closure passed into Task isn’t actually itself MainActor — it has inherited nonisolated from the enclosing function.

I think it might help to translate this into GCD.

func dispatchResponders() {
    // synchronously enqueued
    DispatchQueue.main.async {
        sendAmbulance()
    }

    // synchronously enqueued
    DispatchQueue.main.async(execute: sendAmbulance)

    // not synchronously enqueued!
    DispatchQueue.global().async {
        DispatchQueue.main.async {
            sendAmbulance()
        }
    }
}

Look really closely at that last one! What we are doing there is introducing a new async closure that then calls our MainActor function. There are two steps. This doesn’t always matter, but it certainly could. And if you need to precisely schedule asynchronous work, @isolated(any) can help.