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)表示ab都已经有了数值,才能把和赋予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,可以把数个闭包装进一个队列里,和队列外的程序异步同时运行,而队列里的可以同时运行也可以先后运行,可以启动可以停止。

更多推荐