On the proliferation of try (and, soon, await)

I see how you came to that conclusion, but there is some subtlety here that is really important. When async let (or the add operation of a task group) creates a new child task, it isn't created on the current executor---it is added to the global concurrent executor.

Generally speaking, that global concurrent executor is expected to run the various tasks in parallel: the current implementation uses the Dispatch library's global concurrent queue (of the appropriate task priority) to provide that parallelism.

Now, we also have an implementation of a global concurrent executor that's effectively a run loop: it runs on the main thread and maintains a queue of partial async tasks that it uses to find its next bit of work. There's no parallelism at all, and all of the concurrency is effectively cooperative based on async functions.

From the perspective of the Structured Concurrency proposal, both should be valid implementations, and your choice is likely to depend on your environment. The most appropriate thing for most Swift developers is a global concurrent executor like the global concurrent Dispatch queue that manages a number of OS threads so you get actual parallelism, but @Max_Desiatov was recently asking about support for single-threaded environments as well.

I've taken a note to add some notion of the global concurrent executor into the Structured Concurrency proposal. Because you are right that all async let guarantees is concurrency, but that's because the parallelism is left to the global concurrent executor.

You could certainly implement DispatchQueue.concurrentPerform on top of async let or (much better) task groups.

Just for fun, I went ahead and ported the merge sort implementation from the Swift Algorithm Club site over to use async let, so I could see the parallelism in action. My implementation is in this gist, but the only difference between mine and the original is the sprinkling of async and await in the mergeSort function:

func mergeSort<T: Comparable>(_ array: [T]) async -> [T] {
  guard array.count > 1 else { return array }

  let middleIndex = array.count / 2

  async let leftArray = await mergeSort(Array(array[0..<middleIndex]))
  async let rightArray = await mergeSort(Array(array[middleIndex..<array.count]))

  return merge(await leftArray, await rightArray)
}

(The await on the right-hand side of the async let will go away once we bring the implementation in line with the proposal)

I ran this on my Mac using Swift's main branch from today under Instruments, and the time profile shows that all of the cores are busy with different pieces of this parallel sort. If I were to switch over to the cooperative scheduler, there would be no parallelism but the result would be the same.

Doug

17 Likes