Search code examples
swiftasync-awaitconcurrencyswift-concurrency

What is a "Concurrent Context"?


I currently reading a book "Modern Concurrency on Apple Platforms".

The book uses the term "Concurrent Context" many times without actually explaining the context of such an context.

For example: "Task has got an initalizer with a closure, which itself is concurrent context". Or: "What happens if you don't have a concurrent context? Somewhere up ith the call hierarchy, you will find a situation in which there is no concurrent context at all, to run the code in."

Can someone explain the term "Concurrent Context" in an easy to get way?

Searched the web and read in the mentioned book.


Solution

  • tl;dr

    With Swift concurrency, the term “context” is the execution environment in which the code finds itself. Code that runs outside of Swift concurrency is said to be running in a “synchronous context”. Code that runs within the Swift concurrency system is said to be running in an “asynchronous context”. And two separate asynchronous routines not isolated to the same actor would be construed as being on separate asynchronous contexts.

    For what it is worth, the term “asynchronous context” is used extensively (but never formally defined) in SE-0296 – Async/await, SE-0304 – Structured Concurrency, and SE-0306 – Actors. As far as I can see, Apple does not generally use the term “concurrency context”, but rather “asynchronous context”, but we can easily surmise the author’s intent.


    To comment specifically on the quotations you have shared with us, we might need more fulsome excerpts. But consider your first quotation:

    Task has got an initializer with a closure, which itself is concurrent context

    The Task {…} will create a new top-level task on behalf of the current actor (if any). So, if the Task was created from within a synchronous context, yes, the closure will run on different context, namely an asynchronous context, but one that is isolated to the current actor (if any). But if you use Task {…} from within an actor-isolated asynchronous context, this new task will be isolated to the same actor from which it was invoked. So the details will vary based upon the immediate context in which the Task was created.

    Perhaps this is nit-picking, but either way, I would hesitate to say that the closure “is the concurrent context”. A closure, itself, is not a “context”. It would be more precise to just describe it as a closure that will run on a particular asynchronous context.

    For the sake of completeness, we should note that this Task {…} behavior stands in contrast to Task.detached {…}, which will never run the code on behalf of the current actor.


    Consider the second quotation:

    What happens if you don't have a concurrent context? Somewhere up in the call hierarchy, you will find a situation in which there is no concurrent context at all, to run the code in.

    I am exceedingly unclear as to what the author is trying to say. In Swift concurrency, it does not navigate “up in the hierarchy” to determine what context it is in. (That is how it worked in GCD, one of the reasons that GCD code could be so brittle; but this is not how it works in Swift concurrency.) It just looks at the current function to see to which actor, if any, it was isolated. If the current function is isolated to a particular actor, then the Task {…} will create a new top-level task on behalf of that actor. But if the current function is not actor-isolated, then the Task {…} will not be isolated to any given actor, either, regardless of what happened earlier in the call hierarchy.

    Consider:

    @MainActor 
    class Foo {
        func foo() {
            // This is isolated to the main actor.
    
            Bar().bar()
        }
    }
    
    class Bar {
        func bar() {
            // This synchronous function is not isolated to any actor, so it will
            // happen to just run on whatever thread from which it was called.
    
            // If this is called from `Foo`’s `foo` method, isolated to the main actor,
            // this will happen to also run on the main thread. But if called from
            // somewhere not on the main thread, this will just run on the current
            // thread, whatever it was.
           
            Baz().baz()
        }
    }
    
    class Baz {
        func baz() {
            // If called from the main thread, this will run on the main thread. If 
            // called from some other thread, this will run on whatever thread it was
            // called.
            //
            // The key is, `baz` is not isolated to any particular actor…
    
            Task {
                // … thus, this is not isolated to any particular actor, either.
                //
                // Notably, even if `foo` called `bar`, and `bar` called `baz`, this
                // will *not* run on the main actor.
            }
        }
    }
    

    In this example, foo happened to called bar on the main thread, and bar happened to call baz on the main thread, too. But baz is not isolated to any particular actor, so even though it started on the main thread in this example, its Task {…} is not isolated to the main actor. Please note, I wrote three non-async functions. The details will differ a little if the functions were async, but the key message still holds: The context used by a closure supplied to Task {…} will be dictated by whether the caller was actor-isolated or not.

    Now, maybe your quotation was in reference to some particular code snippet that was not shared with us, so I cannot comment further. But I would seek to disabuse anyone from the idea that random Swift concurrency code looks at the call hierarchy to figure out its context. The context of the function is dictated by whether the current function (or the type of which it is a member) was isolated to a particular actor or not.