2
class ViewModel: ObservableObject { }

struct ContentView: View {
    @StateObject private var model = ViewModel()

    var body: some View {
        Button("Authenticate", action: doWork)
    }

    func doWork() {
        Task.detached {
            for i in 1...10_000 {
                print("In Task 1: \(i)")
            }
        }

        Task.detached {
            for i in 1...10_000 {
                print("In Task 2: \(i)")
            }
        }
    }
}

This is the code described in https://www.hackingwithswift.com/quick-start/concurrency/whats-the-difference-between-a-task-and-a-detached-task.

Since the Tasks in doWork are detached, I expect them to be executed at the same time. The above article also says so.

However, when I run this, Task2 is executed after Task1.
Am I wrong?

user9015472
  • 157
  • 1
  • 8
  • 3
    Have you tried lowering the number of loop iterations you're running to more easily verify whether the second task is truly _only_ running after the first has completed? You may get long runs of T1 and T2 alternating so that it's difficult to tell at a glance that they're actually interleaved. – Itai Ferber Aug 01 '22 at 14:14

2 Answers2

5

By putting the items into detached tasks, you are letting the operating system decide when to schedule them. Apparently, for one of the times you looked, the system decided to schedule them one after the other. There's nothing to say that the next time you look they won't be scheduled in parallel, or so that the second one runs before the first one.

The point is that you don't have any control over it. And the OS may make a different decision tomorrow than it does today. You've ceded that control to the OS by putting the work in Tasks.

Scott Thompson
  • 22,629
  • 4
  • 32
  • 34
  • 2
    To add a concrete example: in a command-line app on macOS, I can't replicate the scenario OP is seeing; the tasks always appear to interleave. But with OP's code in an iOS simulator, I _am_ seeing what OP is seeing, with the tasks running one after another. It's entirely up to the scheduler to make good use of the cooperative thread pool, and depending on the number of available threads and the amount of work it thinks it needs to do, it may or may not run tasks in parallel. For instance, if I change the priority of the tasks relative to one another, I _do_ see them interleave in the sim. – Itai Ferber Aug 01 '22 at 14:27
3

Multiple detached tasks do run concurrently. Consider this example, where I perform a computationally intensive operation 20 times, each in its own task:

import SwiftUI
import os.log

private let log = OSLog(subsystem: "Detached tasks", category: .pointsOfInterest)

struct ContentView: View {
    var body: some View {
        Button("Do work", action: doWork)
    }

    func doWork() {
        os_signpost(.event, log: log, name: #function)

        for i in 0 ..< 20 {
            Task.detached {
                let id = OSSignpostID(log: log)
                os_signpost(.begin, log: log, name: #function, signpostID: id, "start %d", i)
                let value = calculatePi(decimalPlaces: 9)
                print(value)
                os_signpost(.end, log: log, name: #function, signpostID: id, "done")
            }
        }
    }

    // deliberately inefficient calculation of pi using Leibniz series

    func calculatePi(decimalPlaces: Int = 9) -> Double {
        let threshold = pow(0.1, Double(decimalPlaces))
        var isPositive = true
        var denominator: Double = 1
        var value: Double = 0
        var increment: Double

        repeat {
            increment = 4 / denominator
            if isPositive {
                value += increment
            } else {
                value -= increment
            }
            isPositive.toggle()
            denominator += 2
        } while increment >= threshold

        return value
    }
}

We can profile that task, using the “Points of Interest” tool. On the simulator that yields:

enter image description here

Note, that is artificially constraining the cooperative thread pool to two tasks at a time.

On an iPhone 12 Pro Max:

enter image description here

That runs six at a time.

And on an Intel 2018 MacBook Pro:

enter image description here

So, bottom line, it does run concurrently, constrained based upon the nature of the hardware on which you run it (with the exception of the simulator, which artificially constrains it even further).


FWIW, the 10,000 print statements is not a representative example because:

  • Too many synchronizations: The print statements are synchronized across threads which can skew the results. To test concurrency, you ideally want as few synchronizations taking place as possible.

  • Not enough work on each thread: A for loop of only 10,000 iterations is not enough work to properly manifest concurrency. It is quite easy that the first task could finish before the second even starts. And even if it does start interleaving at some point, you might see a couple thousand on one task, then a couple thousand on the next, etc. Do not expect a rapid interleaving of the print statements.

For these reasons, I replaced that for loop with a calculation of pi to 9 decimal places. The idea is to have some calculation/process that is sufficiently intensive to warrant concurrency and manifest the behavior we are looking for.


Perhaps needless to say, if we experience serial behavior if we:

  • move this calculation to an actor; and
  • launch with Task { ... } rather than detached task.

enter image description here

(Note, the horizontal time scale as been compressed to fit the twenty tasks on screen at the same time.)

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I've tried a slightly changed version of Rob's code and noticed that on a simulator the tasks didn't run in parallel. Using a GroupTask it didn't run the code in parallel as well. Then I tried running the same code on a real device and saw that it was running the tasks in parallel. Both Task.detached and GroupTask (even if I don't specify background priority) were running the tasks in parallel on an iPhone. – Ruslan Mansurov Feb 11 '23 at 08:22
  • The simulator has an artificially constrained cooperative thread pool. https://stackoverflow.com/a/72860015/1271826 or https://stackoverflow.com/a/67978029/1271826. – Rob Feb 11 '23 at 17:30