This article is the first of my notes on Swift concurrent programming. It introduces the use and inner meaning of the async and await syntax keywords.

async

Methods that are modified with async are called asynchronous methods. The syntax is as follows:

1
2
3
func my_func() async {
    print("hello kanchuan.com")
}

If a method is both asynchronous and throwing, the async keyword needs to be written in front of the throws keyword.

In terms of syntax alone, the async keyword is simply added to the normal function. When a normal method is modified with the async keyword to become an asynchronous method, the implications are

  1. the await keyword can be used inside the function body of the asynchronous method (or, of course, without await);
  2. when the async method is called elsewhere, the await keyword is used.

await

await means that this is a “possible suspension point”, which indicates to the compiler that this is a possible suspension point (note the word “possible” here, the meaning of “possible” will be explained later). When the program reaches the await code, it abandons the thread(yielding the thread) and “pauses” to wait for the return of the asynchronous method.

  1. the pause here is of the method, not of the thread executing the method, otherwise the point of doing so would be defeated;
  2. await gives up possession of the current thread, which can be scheduled to execute other code;
  3. when the asynchronous method awaiting is finished, it resumes from its “paused” state and will continue executing the code after the await statement.

Not everywhere await can be used

The await keyword can only appear in asynchronous contexts, and there are currently two cases:

  1. within the body of an async asynchronous function;
  2. in the closure of a Task task.

In fact, both of these cases are within a Task, which is the basic unit for performing concurrent tasks, and all functions marked by async are managed through the Task.

Understanding Task

Task is a highly abstract encapsulation of threads, analogous to GCD, except that GCD is a capability provided by the libdispatch open source library and is not supported by the native syntax. Task is natively supported by Swift and will gradually replace GCD in future use Swift as the preferred solution for completing asynchronous tasks.

Task {} is a simplified form of Task.init, where a task created by Task.init inherits the context of the calling thread and executes in the calling thread, whereas a task created by Task.detached executes in a separate thread, completely independent of the calling thread, with its own execution context and resources.

How does await affect the scheduling of threads?

In the design of Swift’s concurrency framework, the concept of threads is further weakened. Thread creation and scheduling are completely hidden and encapsulated by the concurrency framework. This is illustrated by two examples:

Example of using Task.init

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func my_func() async {
    print("before sleep \(Thread.current)")
    try? await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    print("after sleep \(Thread.current)")
}
    
func main_func() {
    print("before main_func")
    Task {
        print("before Task \(Thread.current)")
        await my_func()
        print("after Task \(Thread.current)")
    }
    print("after main_func")
}

In the above code, the main_func method ensures that it is called in the main thread (as does the example below), and the Task closure calls the asynchronous method my_func, which executes the delay. Here is a printout of the above code running.

1
2
3
4
5
6
before main_func
after main_func
before Task <_NSMainThread: 0x600002ccc140>{number = 1, name = main}
before sleep <_NSMainThread: 0x600002ccc140>{number = 1, name = main}
after sleep <NSThread: 0x600002c89ac0>{number = 7, name = (null)}
after Task <_NSMainThread: 0x600002ccc140>{number = 1, name = main}

Example of using Task.detached

Modify the above code by changing Task.init to Task.detached.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func my_func() async {
    print("before sleep \(Thread.current)")
    try? await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    print("after sleep \(Thread.current)")
}
    
func main_func() {
    print("before main_func")
    
    Task.detached { [self] in
        print("before Task \(Thread.current)")
        await my_func()
        print("after Task \(Thread.current)")
    }
    
    print("after main_func")
}

Run and observe the output.

1
2
3
4
5
6
before main_func
after main_func
before Task <NSThread: 0x600002ed1080>{number = 8, name = (null)}
before sleep <NSThread: 0x600002ed1080>{number = 8, name = (null)}
after sleep <NSThread: 0x600002ec43c0>{number = 4, name = (null)}
after Task <NSThread: 0x600002ec43c0>{number = 4, name = (null)}

It can be noted that:

  • the use of await markers changes the thread of code execution;
  • each await acts as a partition barrier, dividing the code into separate ‘chunks’;
  • The thread that executes each ‘chunk’ is indeterminate, scheduled by the concurrency framework, and may be in one thread or in different threads.

Based on these principles, it is important to avoid using traditional synchronisation mechanisms such as semaphores and locks in such situations where the thread of execution is uncertain, and instead take advantage of the features of the Swift concurrency framework itself. The following example will cause a deadlock.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
let lock = NSLock.init()
func my_func() async {
    lock.lock()
    print("before sleep \(Thread.current)")
    try? await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    lock.unlock()
    print("after sleep \(Thread.current)")
}

for i in 0..<5 {
  Task {
    await my_func()
  }
}

Understanding the “possible” in possible suspension points

The reason for the “possible” modifier is that not all await keywords result in a real suspension, and there are some special cases.

In the example above, Task.sleep is called in my_func, but it is perfectly possible to call the function synchronously in my_func. Modify the above example as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func my_func() async {
    print("in my_func \(Thread.current)")
}
    
func main_func() {
    print("before main_func")
    
    Task {
        print("before Task \(Thread.current)")
        await my_func()
        print("after Task \(Thread.current)")
    }
    
    print("after main_func")
}

At this point, we declare my_func to be an asynchronous function, but it executes all synchronous code inside. The result of the run is as follows:

1
2
3
4
5
before main_func
after main_func
before Task <_NSMainThread: 0x600001830a00>{number = 1, name = main}
in my_func <_NSMainThread: 0x600001830a00>{number = 1, name = main}
after Task <_NSMainThread: 0x600001830a00>{number = 1, name = main}

You can see that the code in the Task closure is executed completely synchronously and there is no ‘pause’.