The Beauty of Swift Language — Structured Concurrency
Modern CPUs usually have multiple cores and some computers even have more than one CPUs, which means they can run multiple instructions in parallel. In order to utilise this hardware feature, we need to structure our code in such way that it can run concurrently. Structured concurrency that exists in many modern languages is invented just for this purpose.
Structured concurrency was added into Swift from version 5.5, which allows us to write concurrency code in an expressive way. It’s built on top of threads and offers an easy but powerful solution to asynchronous/parallel programming.
In this article, we are going to compare this feature in Swift and its counterparts in C# and Kotlin.
In order to demonstrate this feature, let us imagine this scenario: in a private tutoring school, the receptionist need to check all the students before the teacher starts teaching in a class. Let’s see how this can be done using structured concurrency in different languages.
Swift
In Swift, to define an asynchronous function, we use keyword async. So, we can define check student method like below:
/// Check if student arrives in time within the maximum waiting time
/// - Parameters:
/// - name: The name of the student.
/// - maxWaitingTime: The maximum waiting time.
/// - Returns: True when arrive within maximum waiting time.
func checkStudent(name: String, maxWaitingTime: TimeInterval = 10) async -> Bool {
…
}
To call an asynchronous method, we use await like so:
await checkStudent(name: "Tim")
1:1 classes
With the defined checkStudent method above, for a private 1:1 class, the teacher just needs to wait one check result like below:
if await checkStudent(name: "Tim") {
teach()
}
Each await is a suspension point, the following code can only run after the awaited result returns. In this case, the if sentence can only be calculated when checkStudent method returns a value.
1:n classes
For a class that has more than one students, the checking process should ideally run in parallel so that the teach can start teaching ASAP. In Swift we use multiple async let and one await to achieve this:
async let resultOfCheckingTim = checkStudent(name: "Tim")
async let resultOfCheckingBob = checkStudent(name: "Bob")
async let resultOfCheckingAlice = checkStudent(name: "Alice")
let results = await \[resultOfCheckingTim, resultOfCheckingBob, resultOfCheckingAlice\]
if results.reduce(false) { $0 || $1 } { // At least one student in class after checking
teach()
}
Using array to handle it in general
You must have found that, in the above code we need to write out all the student names, which is very cumbersome. Task Group comes to rescue for cases like this.
let names = ["Tim", "Bob", "Alice"]
let results = await withTaskGroup(of: Bool.self) { taskGroup in
for name in names {
taskGroup.addTask { await checkStudent(name: name) }
}
var results: [Bool] = []
for await result in taskGroup {
results.append(result)
}
return results
}
if results.reduce(false) { $0 || $1 } { // At least one student in class after checking
teach()
}
The concept of TaskGroup is the source of the power of structured concurrency. All the tasks in the group run in parallel and they will be canceled altogether if we cancel this group.
Also, each task can have its child tasks. Swift use this explicit tree-like relationship between tasks and task groups to handle some behaviours like propagating cancellation and detect errors at compile time. Please check here for more information about TaskGroup from Apple.
C#
async ,await and Task are the one of the answers in Swift to write structured concurrency code. By organising tasks in a tree structure using subtasks, the life cycles of subtasks will be managed automatically.
To define a method that runs synchronically, use async keyword like below:
static async Task<bool> checkStudent(String name, double maxWaitTime = 0)
{
...
}
To call an async method is just like a regular method in syntax. But in order to access the return value of an async method, use await keyword like below:
var tim = await checkStudent("Tim");
// or
var tim = await checkStudent("Tim");
var result = await tim;
1:1 classes
Based on the above definition, the 1:1 classes can be implemented this way:
using System;
using System.Linq;
using System.Threading.Tasks;
public class PrivateTutoring
{
static async Task<bool> checkStudent(String name, double maxWaitTime = 0)
{
...
}
public static async Task Main()
{
if (await checkStudent("Tim")) {
teach();
}
}
}
1:n classes
Similarly, 1:n classes can be written this way:
using System;
using System.Linq;
using System.Threading.Tasks;
public class PrivateTutoring
{
static async Task<bool> checkStudent(String name, double maxWaitTime = 0)
{
await Task.Delay(3000);
return true;
}
public static async Task Main()
{
var resultOfCheckingTim = checkStudent("Tim");
var resultOfCheckingBob = checkStudent("Bob");
var resultOfCheckingAlice = checkStudent("Alice");
bool\[\] results = await Task.WhenAll(resultOfCheckingTim, resultOfCheckingBob, resultOfCheckingAlice);
var result = results.Aggregate((x, y) => x || y);
if(result) {
teach();
}
}
}
Using array to handle it in general
using System;
using System.Linq;
using System.Threading.Tasks;
public class PrivateTutoring
{
static async Task<bool> checkStudent(String name, double maxWaitTime = 0)
{
...
}
public static async Task Main()
{
string\[\] @names = {"Tim", "Bob", "Alice"};
var taskResults = names.Select(x => checkStudent(x));
bool\[\] results = await Task.WhenAll(taskResults);
var result = results.Aggregate((x, y) => x || y);
if(result) {
teach();
}
}
}
Kotlin
Kotlin provides concurrency programming at language level through coroutines and structured concurrency was achieved by calling suspending methods in CoroutineScope. The coroutine scope is very much like TaskGroup in Swift and it manages the life cycles of the coroutines inside it. A CoroutineScope can be embed in another CoroutineScope and form a tree-like structure and the discipline of structured concurrency was enforced through this structure.
The same checkStudent method above could be defined as suspend method in Kotlin like below:
suspend fun checkStudent(name: String, maxWaitingTime: Double = 10.0): Boolean {
…
}
And in order to call a suspending method, we need to have a coroutine scope like below. In Kotlin, coroutine scopes can be build using various of Scope builders like runBlocking and coroutineScope etc.
fun main() = runBlocking {
checkStudent("Tim")
}
1:1 classes
fun main() = runBlocking {
val result = checkStudent("Tim")
if(result) {
teach()
}
}
1:n classes
fun main() = runBlocking {
val resultOfCheckingTim = async { checkStudent("Tim") }
val resultOfCheckingBob = async { checkStudent("Bob") }
val resultOfCheckingAlice = async { checkStudent("Alice") }
val result = resultOfCheckingTim.await() || resultOfCheckingBob.await() || resultOfCheckingAlice.await()
if(result) {
teach()
}
}
Using array to handle it in general
fun main() = runBlocking {
val result = listOf("Tim", "Bob", "Alice")
.map {
async { checkStudent(it) }
}
.map { it.await() }
.reduce { acc, next -> acc || next } // At least one student in class after checking
if(result) {
teach()
}
}
Conclusion
As discussed above, these three languages offer different syntaxes to provide structure concurrency feature. Personally I like the Swift syntax better while Kotlin does provide more flexible scope builders on this regard and C# is also very powder in terms of task management.
It is worth mentioning that this article only touches a surface of this feature in these languages. Furthermore, when we start using this structured concurrency, soon we will face the problem of sharing data among different concurrency tasks, for which, different languages have their own solutions too. For this topic, it is beyond the scope of this article.
Sharing is caring!