Swift的多线程处理(concurrency)
Swift支持多线程处理的功能。也就是说,语句并不是只能执行完一句再执行下一句。可以两句同时执行。同时执行多个语句,在计算机编程中称为“异步执行”(async),而执行完一句再执行下一句是“同步执行”(sync)。
一、async和await
Swift有个关键字叫async,表明一个函数,或者一段语句,是允许异步执行的,也就是说在执行它时,允许同时执行其它语句。现在定义两个异步执行的函数:
func wait2seconds() async -> Int{
print("2 seconds begin")
sleep(2)
return 2
}
func wait3seconds() async -> Int{
print("3 seconds begin")
sleep(3)
return 3
}
这两个函数执行分别需要2秒和3秒。但有async关键字,使得在运行两秒的函数时,三秒的函数可以同时运行。
那么,如何让这两个函数同时运行呢,用常规的语句行吗?
wait2seconds()
wait3seconds()
编译器直接报错:’async’ call in function that does not support concurrency
这是因为,系统默认的运行环境,是不支持异步执行的,所以无法直接运行async的函数。这里,我们用一个Task环境,尾随一个闭包,来让两个函数异步执行。
func runPart1_good(){
Task{ // Remember, async functions must be triggered within Task context
async let a = wait2seconds()
async let b = wait3seconds()
let s = await(a + b)
print(s)
}
}
runPart1_good()
这里,async let a = wait2seconds()运行时,async let b = wait3seconds()也在运行。但是在接下来的句子里有一个字await。await,表示这句话必须在运行完后,才能运行接下来的语句。
分析一下,let s = await(a + b)表示a和b都已经有了数值,才能把和赋予s。但a有值,必须在wait2seconds()运行完了才能实现。b也一样。
运行结果
2 seconds begin
3 seconds begin
5
等了3秒,print(s)运行了。显然,2秒的任务和3秒的任务同时运行,全部结束的时间取决于最长的任务,所以等3秒。
反过来说,如果这样写呢?
func runPart1_bad(){
Task{
let a = await wait2seconds()
let b = await wait3seconds()
let s = await(a + b)
print(s)
}
}
这样的话,就是要等5秒,因为每句话都要等完成后才能进行下一句。
二、class和actor
问题描述
多线程处理程序,能加快效率。但是,有时会导致两个线程做重复的事情,以至于效果和一个线程做没有区别,或自相矛盾。这样的问题,其实在修改某个对象的属性时很常见。想象一下两个线程需要修改某个文件,但修改要基于已有的文件,那就有这样的逻辑问题:
为什么两个人的要求会互相矛盾?不能同时满足吗?其实,问题就是在于,两个人说的“加个…“,都是基于最初的一个正方形的。所以,如果是”先加个三角,再加个五角星“,那么正确的方法应该是:把“加三角”和“加五角星”作为有先后的两步“。”加五角星“必须在”加三角“完成后,基于加了三角的图形,再进行。因此,“加图形”的操作,不能异步同时进行。
下面构建一个类,关于学生的成绩:
class studentGrade{
var name = ""
var grade = 0
init(_ n:String, _ g:Int){
name = n
grade = g
}
func addMark() async -> Int{
let currGrade = grade
var newGrade = currGrade + 10
sleep(5)
self.grade = newGrade
return 0
}
}
类里的方法addMark()是个支持异步操作的函数,用于把分数加10分。
现在我同时运行两个addMark()线程,都完成后查询新成绩。学生成绩原为90分。
func badPlusGrade(){
let Iven = studentGrade("Iven", 90)
Task{
async let result = Iven.addMark()
async let result2 = Iven.addMark()
var newMarkFinish = await(result + result2)
print("After two runs of addMark, the result is still \(Iven.grade)")
}
}
运行结果
After two runs of addMark, the result is still 100
仍然只是100分,似乎addMark()只运行了一次。原因就是在于:addMark()两次由于同时运行,当第二个addMark()获取原分数时,第一个addMark()还没结束,所以学生的分数仍然是90分,因此第二个addMark()也是基于90分而不是已经加了10分后的100分。
解决方案
关于这种情况,Swift提供一种特殊的类,不再是class,而是actor。actor也是一种类,但和class不同,对于任何一个对象,里面的涉及对象里的属性的函数,只能同时被运行一次,必须运行完了才能运行下一次。如果要允许同时运行两次,请在函数前加上nonisolated修饰词。
actor studentGradeA{
var name = ""
var grade = 0
init(_ n:String, _ g:Int){
name = n
grade = g
}
func addMark() async -> Int{
let currGrade = grade
var newGrade = currGrade + 10
sleep(5)
self.grade = newGrade
return 0
}
nonisolated func getName() async -> String{
sleep(5)
return await self.name
}
}
现在再运行以下代码
func goodPlusGrade(){
let Iven = studentGradeA("Iven", 90)
Task{
async let result = Iven.addMark()
async let result2 = Iven.addMark()
var newMarkFinish = await(result + result2)
print("After two runs of addMark, the result is now \(await Iven.grade)")
async let name1 = Iven.getName()
async let name2 = Iven.getName()
print("Name \(await name1 + name2) is read within 5 seconds")
}
}
goodPlusGrade()
运行结果
After two runs of addMark, the result is now 110
Name IvenIven is read within 5 seconds
在这里,等待10秒后,学生的分数被增加了20,现在是110。这是因为async let result = Iven.addMark()语句运行后,直到完成后,才允许运行第二次addMark(),即async let result2 = Iven.addMark(),因此该加分方法的原分数是基于第一次加分后的结果。
与此同时,两次getName()却是同时运行的,所以5秒后就输出了。这是因为这个函数前面有nonisolated修饰词,允许两次同时运行。显然,这个函数不更改参数,所以没有两个线程同时修改参数导致的冲突。
三、派遣队列(dispatch queue)
在Swift中,也有另一种管理异步执行任务的类,叫做DispatchQueue。
一个DispatchQueue对象,有两个常用方法。
(一)队列加入闭包的方法
一个叫做async,一个叫做sync,都尾随一个闭包。
对于一个DispatchQueue对象Queue1,当运行以下代码时:
Queue1.async{
Something()
}
Rest()
他会把这个带Something()的闭包加入Queue1。加入完成后,这段代码就结束了。至于这个闭包里的东西,它是否在运行,或者运行完成,不影响之后的Rest()的正常运行。换言之,无论Something()有多长,Queue1.async{}语句都会立刻结束,Rest()都会立刻开始。
那么对于sync呢?运行以下代码时:
Queue1.sync{
Something()
}
Rest()
这个代码包括两件事:一是把这个带Something()的闭包加入Queue1。二是运行闭包直至完毕。只有闭包运行完成了,Queue1.sync{}才算结束,Rest()才能开始。这是async和sync的主要区别。
(二)队列的创建和属性
下面先创建两个10秒的函数,用它们来解释DispatchQueue如何使用。
func exe1(){
sleep(10)
}
func exe2(){
sleep(10)
}
默认队列
现在,产生一个DispatchQueue对象。
let QueueSerial = DispatchQueue(label: "queue1")
这个对象是默认的DispatchQueue对象。默认情况下,对象里只要有通过async加入的闭包,就会开始运行这些闭包。且对于通过async加入的闭包,会按顺序运行,而非同时运行两个闭包。
运行以下代码:
QueueSerial.async {
print("Execute thread 1...")
exe1()
print("Thread 1 done")
}
QueueSerial.async {
print("Execute thread 2...")
exe2()
print("Thread 2 done")
}
print("rest")
运行结果:
Execute thread 1...
rest
Thread 1 done
Execute thread 2...
Thread 2 done
显然,thread 1运行完了,才运行thread 2。但是之后的print("rest")却早就开始了。这是因为对于QueueSerial里面的闭包,它要一个接一个地按顺序不同时运行,但对于QueueSerial以外的程序,只要QueueSerial.async语句运行完了,即“加入队列”的过程做完了,之后的程序照常运行,和QueueSerial里面的闭包运行互不冲突。换言之,DispatchQueue里面的“按顺序同步运行”只针对里面的闭包,和对外“异步同时运行”不矛盾。对外也“顺序同步运行”的方式是通过sync加入闭包。
同时运行
那么,如何让DispatchQueue里面的闭包也同时异步运行呢?DispatchQueue里面有一种attributes叫做.concurrent,在初始化时定义。
let QueueConcurrent = DispatchQueue(label:"queue2", attributes: [.concurrent])
所以QueueConcurrent里面的闭包都会同时运行。
QueueConcurrent.async{
print("Execute thread 1...")
exe1()
print("Thread 1 done")
}
QueueConcurrent.async {
print("Execute thread 2...")
exe2()
print("Thread 2 done")
}
运行结果
Execute thread 1...
Execute thread 2...
Thread 1 done
Thread 2 done
初始闲置
除此之外,还有一个attribute可以让队列不在有闭包加入后,就主动运行里面的内容。用.initiallyInactive
let QueueInactive = DispatchQueue(label:"queue3", attributes: [.initiallyInactive])
QueueInactive.async {
print("Execute thread ...")
exe1()
print("Thread done")
}
此时,闭包不会运行。要让闭包运行,应该先用activate()方法让队列启动。
print("Not started yet, wait for 5 seconds")
sleep(5)
QueueInactive.activate()
运行结果
Not started yet, wait for 5 seconds
Execute thread ...
Thread done
等待5秒,activate后,闭包才开始运行。
那么,如果在队列尚未activate前,就通过sync加入闭包,会怎么样呢?这里把队列暂停(suspend)
QueueInactive.suspend()
QueueInactive.sync {
print("Execute thread ...")
exe1()
print("Thread done")
}
这段代码及其之后的代码永远不会运行。首先,由于QueueInactive已经被停止或尚未启用,里面的闭包不运行。另外,由于sync要求闭包里内容运行了才能继续之后的代码,所以会等待不运行的东西,造成死锁。
(三)全局队列
最后再介绍一个程序自动构建的队列,即全局队列。
let backgroundQueue = DispatchQueue.global()
func wait5(){
print("Begin with wait 5")
sleep(5)
print("Finish wait 5")
}
func waitback3(){
print("Begin with waitback 3")
sleep(3)
print("Finish waitback 3")
}
print("Part 3 further -- global dispatch queue")
backgroundQueue.async {
waitback3()
}
wait5()
运行结果:
Part 3 further -- global dispatch queue
Begin with waitback 3
Begin with wait 5
Finish waitback 3
Finish wait 5
也是一样,队列里和队列外同时运行,互不干涉。
总结
Swift里的队列,可以用async和await处理。对于类,为了防止两个线程同时更改同一个属性造成冲突的现象,通常建议使用actor而非class。另外Swift有一个工具叫DispatchQueue,可以把数个闭包装进一个队列里,和队列外的程序异步同时运行,而队列里的可以同时运行也可以先后运行,可以启动可以停止。
更多推荐


所有评论(0)