skip to Main Content

In the Apple Docs, it says

Tasks can start running immediately after creation; you don’t explicitly start or schedule them.

However, in my code, the Task only starts running when the function that calls it goes out of scope.

func test() {
    Task {
        print("in task")
    }
    for _ in 0 ..< 10_000_000 { }
    print("done counting")
}

This code when executed hangs / waits for a certain amount of time and then always prints:
done counting
in task

I expected it to print
in task
done counting

Can someone explain what I’m missing from what’s written in the Apple Docs?

3

Answers


  1. You’re probably testing in the simulator, which serializes Swift Concurrency to a single thread. Note the phrase "can start running." It does not promise that it will.

    You also are blocking the whole thread with your loop. You may want to add a Task.yield() in the block to allow other things to run. Structured concurrency is all about cooperating tasks that await when they would otherwise block. It’s not specifically about parallelism of CPU-intensive tasks. Tasks are not intended to block their thread (doing so violates their contract to "make progress").

    Tasks also default to the same context that they are started in. If you want a new independent task, you want to use Task.detached {}.

    Login or Signup to reply.
  2. In Swift’s concurrency model, tasks created using the Task API are not executed immediately upon creation. Instead, they are added to the system’s task scheduler and executed asynchronously at some point in the future.

    In the code snippet you provided, the Task block is created but not executed immediately. The for loop following the Task block is executed before the Task block is scheduled for execution.

    If you want to wait for the task to complete before executing the rest of the code, you can use the await keyword to pause the current task and wait for the child task to complete. For example:

       func test() async {
        await Task {
            print("in task")
        }
        for _ in 0..<10_000_000 { }
        print("done counting")
    }
    

    In this version, the await keyword is used to pause the current task and wait for the child task to complete before the print("done counting") statement is executed. This ensures that the message from the Task is printed before the "done counting" message.

    Login or Signup to reply.
  3. You say elsewhere that Task {…} “starts running on its own thread asynchronously”. It doesn’t. It creates a new task for the current actor.

    As the docs for Task {…} says, it:

    Runs the given nonthrowing operation asynchronously as part of a new top-level task on behalf of the current actor.

    But an actor can only be running one thing at a time. So, that means that until the current actor finishes execution (or hits an await “suspension point”), this Task will simply not have an opportunity to run.

    If you want it to start something on a separate thread, consider Task.detached {…}. The docs for detached tell us that it:

    Runs the given throwing operation asynchronously as part of a new top-level task.

    So, a detached task may avail itself of a thread pulled from the cooperative thread pool, running in parallel with the current code. (Obviously, if the cooperative thread pool is busy running other stuff, even Task.detached might not start immediately: It depends upon your hardware and how busy the the cooperative thread pool might be with other tasks.)

    But the following will likely show “in task” before “done counting”. (Technically, there is a “race” and either message could appear first, but presumably your for loop is slow enough that you will consistently see “in task” message first.)

    func test() {
        Task.detached {
            print("in task")
        }
        for _ in 0 ..< 10_000_000 { }
        print("done counting")
    }
    

    Needless to say, you never would spin like this unnecessarily. I presume that the spinning for loop is here merely to manifest the problem at hand. But in more practical examples, where you have an await suspension point, the problem simply disappears. Consider the following:

    func downloads(_ urls: [URL]) async {
        Task {
            print("in task")
        }
    
        for url in urls {
            await download(url)
        }
    
        print("done counting")
    }
    

    Here, the detached task is not needed because we have an await in this routine, so the is ample opportunity for the Task to be run. This obviously is a contrived example (i.e., I generally would avoid unnecessary unstructured concurrency), but hopefully it illustrates that if your loop doesn’t actually block, but rather has await suspension points, using the current actor is not a problem.


    Note, the detached task example earlier is a bit backwards from the common pattern. (I did that to preserve your code order.) Usually, it is the blocking code that would go inside the detached task. E.g., consider an example where we want to call some image processing service that is synchronous, computationally intensive, and blocks the caller; this is a more practical example of a detached task, explicitly avoiding the blocking of the current actor:

    func process(_ image: UIImage) async throws {
        print("starting")
    
        processedImage = try await Task.detached {
            ImageService.shared.convertToBlackAndWhite(image)
        }.value
    
        print("done")
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search