Swift并发编程 - 理解 async 和 await

 原创    2023-06-30

本文是我学习 Swift 并发编程的第一篇笔记,文章从几个不太好理解的点,介绍了async 和 await 语法关键字的使用方法和内在含义。

async

使用 async 修饰的方法,称为异步方法(asynchronous method)。语法如下:

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

如果一个方法既是异步又是 throwing 的,需要把 async 写在 throws 关键字前面。

单从语法上看,只是在普通函数中增加了 async 关键字。当普通方法使用 async 关键字修饰变成异步方法后,带来的影响是:

  1. 可以在异步方法的函数体内部使用 await 关键字(当然也可以不使用 await);
  2. 其它地方在调用这个异步方法时,需要使用 await 关键字。

await

await 表示此处是一个“possible suspension points",它指示编译器此处是可能的暂停点(注意这里是“可能”,后面会解释“可能”的含义)。当程序运行到 await 的代码时,会放弃当前线程(yielding the thread),“暂停”以等待异步方法的返回:

  1. 这里的暂停是指方法的暂停,而不是执行方法的线程的暂停,不然就失去了这么做的意义;
  2. await 会让出对当前线程的占有,这个线程可以被系统安排执行其它代码;
  3. await 等待的异步方法执行完成以后,从“暂停”状态恢复,将继续执行 await 语句后的代码。

不是在任意地方都能用 await

await 关键字只能出现在异步上下文环境中,目前有两种情况:

  1. 出现在 async 异步函数体内;
  2. 出现在 Task 任务的闭包中。

实际上,这两种情况都是在 Task 中。Task 是执行并发任务的基本单元,所有被 async 标记的函数都是通过 Task 管理的。

理解 Task

Task 是对线程的高度抽象封装,可以类比 GCD,只不过 GCD 是由 libdispatch 开源库提供的能力,并不是来自原生语法的支持。Task 是 自 Swift 原生支持的,在未来的使用中,将逐渐替代 GCD 成为 Swift 中完成异步任务的首选方案。

Task {} 是 Task.init 的简化形式,由 Task.init 创建的任务会继承调用线程的上下文并在调用线程中执行;而由 Task.detached 创建的任务是在独立的线程中执行,与调用线程完全独立,拥有自己的执行上下文和资源。

await 如何影响线程的调度?

在 Swift 的并发框架设计中,进一步弱化了线程的概念。线程的创建、调度完全交由并发框架隐藏并封装。下面用两个例子说明:

使用 Task.init 的例子

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")
}

上述代码,main_func 方法确保是在主线程调用的(下面的例子也是如此),Task 闭包中调用了异步方法 my_func,里面则执行了延迟。下面是上述代码的运行打印结果:

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}

使用 Task.detached 的例子

修改上述代码,将 Task.init 改成 Task.detached

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")
}

运行观察打印输出:

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)}

可以注意到:

  • 使用 await 标示后,改变了代码的执行线程;
  • 每一个 await 就像一个分割屏障,将代码分成一个一个的独立「块」;
  • 执行每一个「块」的线程是不确定的,由并发框架调度,有可能在一个线程,也有可能在不同的线程。

基于以上原理,在这种执行线程不确定的情形下要尽量避免使用信号量、锁等传统同步机制,而是利用 Swift 并发框架本身的特性。如下示例就会造成死锁。

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()
  }
}

理解 possible suspension points 中的 possible

之所以加上 "possible" 这个修饰词,是因为并非所有的 await 关键字都会导致真正的暂停,有一些特例情况。

在上述示例中,my_func 里 调用了 Task.sleep,实际上,我们也完全可以在 my_func 同步调用函数。将上述例子修改如下:

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")
}

此时,我们声明 my_func 是异步函数,但它里面执行的却都是同步代码。打印结果如下:

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}

可以看到 Task 闭包中的代码是完全同步执行的,并没有发生「暂停」。

参考文档:

Swift 并发初步
Swift.org:Concurrency

相关文章:

拥抱 Swift 和 SwiftUI
Swift并发编程 - 理解结构化和结构化并发的底层技术
NSUserDefaults的suitename
Thread Sanitizer 的原理和使用
iOS 13 中对 dyld 3 的改进和优化

发表留言

您的电子邮箱地址不会被公开,必填项已用*标注。发布的留言可能不会立即公开展示,请耐心等待审核通过。