async
使用 async 修饰的方法,称为异步方法(asynchronous method)。语法如下:
func my_func() async {
print("hello kanchuan.com")
}
如果一个方法既是异步又是 throwing 的,需要把 async 写在 throws 关键字前面。
单从语法上看,只是在普通函数中增加了 async 关键字。当普通方法使用 async 关键字修饰变成异步方法后,带来的影响是:
- 可以在异步方法的函数体内部使用 await 关键字(当然也可以不使用 await);
- 其它地方在调用这个异步方法时,需要使用 await 关键字。
await
await 表示此处是一个“possible suspension points",它指示编译器此处是可能的暂停点(注意这里是“可能”,后面会解释“可能”的含义)。当程序运行到 await 的代码时,会放弃当前线程(yielding the thread),“暂停”以等待异步方法的返回:
- 这里的暂停是指方法的暂停,而不是执行方法的线程的暂停,不然就失去了这么做的意义;
- await 会让出对当前线程的占有,这个线程可以被系统安排执行其它代码;
- await 等待的异步方法执行完成以后,从“暂停”状态恢复,将继续执行 await 语句后的代码。
不是在任意地方都能用 await
await 关键字只能出现在异步上下文环境中,目前有两种情况:
- 出现在 async 异步函数体内;
- 出现在 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 闭包中的代码是完全同步执行的,并没有发生「暂停」。
留言板