5

Swift并发编程 - 理解 async 和 await

 1 year ago
source link: https://kanchuan.com/blog/184-swift-async-await.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

相关文章:

iOS系统如何获取用户的本机手机号
iOS Link Map
iOS Flutter MethodChannel 双向通信
iOS Appium自动化测试框架原理简析
iOS安全:Tweak clang: warning: libstdc++ is deprecated


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK