Actor with Task

Hi,

I have the following actor with a function f1

Question

  1. Is it guaranteed that thread executing f1 and the task in f1 will be the same?
  2. If yes, then is that the reason why no await is required calling increment()?

Observation

When the actor is changed to a class I see that the they are executed in different threads

Code

actor Counter {
    var value = 0
    
    func increment() {
        value = value + 1
    }
    
    func f1() {
        print("f1 thread = \(Thread.current)")
        Task {
            print("f1 thread = \(Thread.current)")
            increment()
        }
    }
}

Output

f1 thread = <NSThread: 0x106133500>{number = 2, name = (null)}
f1 thread = <NSThread: 0x106133500>{number = 2, name = (null)}
2 Likes

No. In general, you can't (and shouldn't) make any assumptions about the thread on which an async function runs. The concurrency runtime will schedule non-isolated async functions and actor functions on any thread in its cooperative thread pool. (Exception: @MainActor functions run on the main thread because MainActor is implemented with a custom executor that guarantees this)

The reason you don't need await here is that Task { … } inherits the actor context from the surrounding context. So the call to increment() doesn't need to perform an actor/executor switch because the task is already isolated to the actor.

In your case, this means you can be certain that the Task { … } won’t start running until f1 completes because both are isolated to the same actor. Again, this doesn't guarantee anything about the thread the code runs on.

4 Likes

Thanks a lot @ole

I have some doubts, please bear with me, I keep getting confused.

The reason you don't need await here is that Task { … } inherits the actor context from the surrounding context. So the call to increment() doesn't need to perform an actor/executor switch because the task is already isolated to the actor.

Questions

  1. What does inheriting context mean? (I see / hear this in the documentation and WWDC videos but I am not sure I fully understand it)

  2. My interpretation from your answer is that Task { ... } though it could run on a different thread but still it ensures the actor isolation (synchronisation), however detached task doesn't ensure actor isolation. Is my understanding correct?

1 Like

The context is the current actor (or "no actor" if an unstructured task is started from a non-actor-isolated function). In addition, Task { … } also inherits priority and any task-local values that might have been set from its surrounding context.

Task.detached doesn't inherit any of those things.

Correct.

(Note that the inherited actor isolation does not extend to non-actor-isolated async functions the task calls in its closure. As of Swift 5.7, these will run on the default cooperative executor (not on the actor). Read SE-0338 for the details.)

4 Likes

Thanks a lot @ole for patiently answering my questions.

There is a lot of new stuff I am learning.

I think (I might be wrong) inheriting context will inherit the actor's executor which is what guarantees the actor isolation.

After reading SE-0338 my understanding is that it is to to free up the actor's executor, which earlier once switched to the actor's executor stayed on longer than needed. It is fascinating how much goes on behind the scenes :exploding_head: lots more for me to learn and understand.

How to refer (name) to Task { ... }?

My understanding is unstructured task is of 2 types Task { ... } and detached task.

What is the correct way to refer to a Task { ... }, is it called as non-detached task or unstructured child task?

1 Like

Yeah, I agree that we don't have a really good name for this.

You should avoid saying "child task" when talking about unstructured concurrency. Child tasks are a concept of structured concurrency (task groups and async let). Their central feature is that the lifetime of the child task is bound to a scope and that the parent task can't leave this scope until all child tasks have completed.

I would call Task { … } an "unstructured task". I think this is good enough to distinguish it from a "detached task". The complication is that both unstructured tasks and detached tasks are a form of unstructured concurrency, but I don't have a better term.

3 Likes

Thanks a lot for that clarification especially why it shouldn't be called as a "child task"