大约1年前,个人团队开始了一个新的项目。此次咱们想使用咱们以前项目的全部知识。其中一个决定就是:咱们想将整个 model API 异步化。这将容许咱们在不影响 APP 其他部分的状况下,改变整个 model 的实现。若是咱们的 APP 能够去处理异步调用,那么咱们就不须要关心是否与后端通讯、是否缓存数据到数据库了(译者注:由于是异步调用,因此咱们不用担忧网络加载、缓存到数据库的操做阻塞了主线程
)。它也能使咱们实现并发。html
做为开发者,咱们必须去理解并行(parallelism
)和并发(concurrency
)的含义。不然,咱们可能会犯一些很严重的错误。如今,让咱们一块儿学习如何并发编程吧!git
那么,同步和异步到底有什么不一样之处呢?假设咱们有一堆item,当对这些item进行同步处理的时候,咱们先从第一个item开始,而后再完成第二个,以此类推。它的执行顺序和FIFO( First In,First Out )的队列是同样的,先进先出。github
转换为代码:method1()
的每一个语句按顺序执行。数据库
// 执行顺序 statement1() -> statement2() -> statement3() -> statement4()
func method1() {
statement1()
statement2()
statement3()
statement4()
}
复制代码
因此,同步意味着:一次只能完成一个item。
编程
相比之下,异步处理能够在同时处理多个item。例如:它会处理item1,而后暂停item1,处理item2,而后继续并完成item1。swift
用一个简单的callback来举个栗子,咱们能够看到statement2会在执行callback以前执行。后端
func method2() {
statement1 {
callback1()
}
statement2
}
//译者注:如咱们经常使用的URLSession
func requestData() {
URLSession.shared.dataTask(with: URL(string: "https://www.example.com/")!) { (data, response, error) in
DispatchQueue.main.async {
print("callback")
}
}.resume()
print("statement2")
}
requestData()
//打印顺序 statement2 callback
复制代码
并发与并行一般能够互换使用(即便Wiki百科也有用错的地方。。),这就致使了一些问题,若是咱们能清晰的知道二者的含义的话,咱们就能够避免这些问题。让咱们举例说明:数组
试想一下:咱们有一堆盒子,须要从A点运送到B点。咱们可使用工人完成这项工做。在同步环境下,咱们只能用1个工人去作这件事情,他从A点拿起一个盒子,到B点放下盒子。缓存
若是咱们要是同时能雇佣多个工人,他们将同时工做,从A点拿起一个盒子,到B点放下盒子。很明显,这将极大的提高咱们的效率。只要至少2位工人同时运输盒子,他们就是在作并行处理。bash
并行就是同时进行工做。
若是咱们只有一个工人,咱们想让他多作几件事,那么发生什么呢?那么咱们就应该考虑在处理状态下有多个盒子了。这就是并发的含义,它就比如把从 A 点到 B 点的距离分割为几步,工人能够从 A 点拿一个盒子,走到一半放下盒子,再回到 A 点去拿另外一个盒子。
使用多个工人咱们可让他们都带有不一样距离的盒子。这样咱们能够异步处理这些盒子。若是咱们有多个工人那么咱们能够并行处理这些盒子。
如今,并行和并发的区别就比较明了了。并行指的是同时进行工做;并发指的是同时工做的选择,它可使并行,也能够不是。咱们大多数的电脑和手机设备能够进行并行处理(取决于它是几核的),可是软件确定是并发工做的。
不一样的操做系统提供不一样的工具供你使用并发。在iOS,咱们默认的工具: 进程和线程,因为OC的历史缘由,也有 Dispatch Queues。
进程是你 app 的实例。它包含执行你 App 所需的全部的东西,具体包含:你的栈、堆和全部的资源。
尽管 iOS 是一个多任务的 OS ,可是它不支持一个 App 使用多个进程,所以你只有一个进程。可是 MAC OS 不一样,你可使用 Process 类去建立新的子进程。 它们与父进程无关,但包含父进程建立子进程时父进程所拥有的全部信息。若是您正在使用macOS,这里是建立和执行进程的代码:
let task = Process()
task.launchPath = "/bin/sh" //executable you want to run
task.arguments = arguments //here is the information you want to pass
task.terminationHandler = {
// do here something in case the process terminates
}
task.launch()
复制代码
thread 相似于轻量级的进程。相比于进程 ,线程在它们的父进程中共享它们的内存。这样就会致使一些问题,好比两个线程同时改变一个变量。当咱们再次读取改变量的值得时候,咱们会获得没法预知的值。在 iOS (或者其余符合 POSIX 的系统)中,线程是被限制的资源,一个进程同时最多有用64个线程。你能够像这样建立并执行线程:
class CustomThread: Thread {
override func main() {
do_something
}
}
let customThread = CustomThread()
customThread.start()
复制代码
因为咱们只有一个进程而且最多只能使用64个线程,因此必须使用其余的方法去使代码进行并发处理。 Apple 的解决方案就是 dispatch queue 。你能够向 dispatch queue 中添加任务,而后期待在某一时刻被执行。 dispatch queue 有不一样的类型:
这不是真正的并发,对吧?尤为是串行队列,咱们的效率并无任何的提升。并发队列也没有使任何事情变得容易。咱们确实拥有线程,因此重点是什么?
让咱们考虑一下,若是咱们有多个队列会发生什么呢。咱们能够在线程上运行多个队列,而后在咱们须要的时候向其中一个队列添加任务。让咱们开一下脑洞,咱们甚至能够根据优先级和当前工做量来分发须要添加的任务,从而优化咱们的系统资源。
Apple 把上述的实现称为 Grand Central Dispatch ,简称 GCD 。在 iOS 它具体是如何操做呢?
DispatchQueue.main.async {
// execute async on main thread
}
复制代码
GCD 最大的优势就是:它改变了并发编程的思惟模型。你使用它的时候不须要考虑 thread ,你只须要把你须要执行的任务添加到不一样的队列中,这使并发编程变得更加容易。
Operation Queue 是 Cocoa 对 GCD 的更高一级的抽象。你能够建立 operation 而不是一些 block 块。它将把 operation 添加到队列中,而后按照正确的顺序执行它们。关于队列分别有如下类型:
let operationQueue: OperationQueue = OperationQueue()
operationQueue.addOperations([operation1], waitUntilFinished: false)
复制代码
你能够经过 block 或者子类的方式来建立 operation 。若是你使用子类的方式建立,不要忘记调用 finish ,若是忘记调用,则 operation 将会一直执行。
class CustomOperation: Operation {
override func main() {
guard isCancelled == false else {
finish(true)
return
}
// Do something
finish(true)
}
}
复制代码
operation 的优点就是你可使用依赖,若是 A 依赖于 B 的结果,那么在获得 B 的结果以前, A 不会被执行。
//execute operation1 before operation2
operation2.addDependency(operation1)
复制代码
Run Loop 跟队列相似。系统队列运行全部的工做,而后在开始的时候重启,例如:屏幕重绘,经过 Run Loop 完成。这里咱们须要注意一点,它们不是真正的并发方法,它们是在一个线程上运行的。它可使你的代码异步执行,同时减去你考虑并发的负担。不是每一个线程都有 Run Loop ,主线程的 Run Loop 是默认开启的,子线程的 Run Loop 须要手动建立。
当你使用 Run Loop 的时候,你须要考虑在不一样 mode 下的状况。举个栗子,当你滑动你的设备的时候,主线程的 Run Loop 会改变并延时全部进入的事件,当你中止滑动的时候, Run Loop 将会切换为默认的 mode ,而后处理事件。input source 对 Run Loop 来讲是必要的,不然,每一个执行操做都会马上结束。因此不要忘了这个点。
关于真正轻量级的线程有一个新的想法,可是它尚未被 Swift 实现。详情能够点击这里。
咱们研究了由操做系统提供的全部不一样的元素,这些元素能够建立并发。可是如上所述,这也会形成不少问题。最容易碰到的同时也是最难识别的问题就是:多个并发任务同时访问同一资源。若是没有机制去处理这些访问,则可能一个任务写入一个值。当第一个任务读取这个值的时候,它期待的是本身写入的那个值,而不是其余任务写入的值。因此,默认的方法是锁住资源的访问来阻止其余线程在资源锁定的时候来访问它。
在了解各类锁机制之间的不一样之处以前,咱们须要先了解一下线程优先级。正如你所想的,线程能够设置高优先级和低优先级,这意味着高优先级的会比低优先级的先执行。当一个低优先级的线程锁住一个资源的时候,若是一个高优先级的线程来访问该资源,高优先级的线程必须等解锁,这样低优先级的线程的优先级就会增长。这就叫作优先级反转,但这会致使高优先级的线程一直等待,由于它永远不会被执行。因此咱们须要注意避免形成这种状况。
想象一下,你如今有两个高优先级的线程一、线程2和一个低优先级的线程3。若是线程3阻塞线程1访问资源,线程1必须去等待。由于线程2有更高的优先级,它的任务会被先执行完。在没有结束的状况下,线程3将不会被执行,所以线程1将被无限期地阻塞。
优先级反转的解决方案是优先级继承。在这种状况下,若是线程1被线程3阻塞,它会将本身的优先级交给线程3。因此线程3和线程2都有高优先级,能够一块儿执行(依赖于OS)。当线程3解锁资源的时候,再将优先级还给线程1,这样线程1将会继续原来的操做。
原子性包含和数据库上下文中的事务相同的思想。你想一次性写入一个值,做为一个操做。32位编译的应用程序,在使用int64_t而没有原子性时,可能会有奇怪的行为。让咱们详细看看发生了什么:
int64_t x = 0
Thread1:
x = 0xFFFF
Thread2:
x = 0xEEDD
复制代码
非原子性的操做会形成在线程1中写入x,可是由于咱们在32位系统上工做,咱们不得不将写入的x分割成 0xFF。
当线程2决定在同一时间写入x的时候,会发生下面的操做进行:
Thread1: part1
Thread2: part1
Thread2: part2
Thread1: part2
复制代码
最后咱们会获得:
x == 0xEEFF
复制代码
既不是 0xFFFF 也不是 0xEEDD。
使用原子性,咱们建立一个单独的事务,会产生如下的行为:
Thread1: part1
Thread1: part2
Thread2: part1
Thread2: part2
复制代码
结果是,x包含线程2设置的值。 Swift 自己没有实现 atomic 。你能够在这里添加一个建议,可是如今,你必须本身实现它。
锁是一种简单的方法,用来阻止多个线程访问同一资源。首先检查线程是否能够进入被保护的部分,若是能够进入,它将锁住被保护的资源,而后进行该线程操做。等线程的操做执行完,它会解锁该资源。若是进入的线程碰到锁住的部分,它会等待解锁。这有点相似于睡眠和唤醒,以检查资源是否被锁。
在 iOS ,能够经过 NSLock 来实现这种机制。须要注意一点:你解锁的线程和你锁住的线程必须是同一线程。
let lock = NSLock()
lock.lock()
//do something
lock.unlock()
复制代码
还有其余类型的锁,好比递归锁 (recursive lock) 。它能够屡次锁住同一资源,而且必须在锁定的时候释放它。在这整个过程当中,其它线程是被排除在外的。
还有一个就是读写锁 (read-write lock),对于须要大量线程读取,而不须要大量线程写入的大型 App ,这是颇有效的一种机制。只要没有线程写入,则全部线程均可访问。只要有线程想写入,它将锁定全部线程的资源。在解锁以前全部线程都不能读取。
在进程级别,还有一个分布式锁 (distributed lock) 。不一样之处在于,若是进程被阻止,它只会将其报告给进程,而且进程能够决定如何处理这种状况。
锁由多个操做组成,这些操做使线程处于休眠状态,直到线程再次启动为止。这会致使 CPU 的上下文更改 (推送注册等等,去存储线程的状态)。这些改变须要不少计算时间,若是你有真正很小型的操做去保护,你可使用自旋锁。它的基本思想就是只要线程在等待,就让它轮询锁( poll the lock )。这比休眠一个线程须要更多的资源。同时,它绕过了上线文的改变,因此在小型操做上更加快。
这个理论上听着不错,可是 iOS 老是出人意料。 iOS 有一个概念叫作 Quality of Service (QoS)。使用它,可能形成低优先级的线程根本不会执行的状况。在这样的线程上设置一个自旋锁,当一个更高优先级的线程试图访问它的时候,会形成高优先级的线程覆盖低优先级的线程,所以,没法解锁所需资源,从而致使阻塞本身。因此,自旋锁在 iOS 是非法的。
互斥跟锁比较像,不一样之处在于,它能够访问进程而不只仅是访问线程。悲催的是你不得不本身实现它,Swift 不支持互斥。你可使用 C 的pthread_mutex
。
var m = pthread_mutex_t()
pthread_mutex_lock(&m)
// do something
pthread_mutex_unlock(&m)
复制代码
信号量是一种支持线程同步中的互斥性的一种数据结构。它由计数器组成,是一个先进先出的队列,有wait()
和signal()
函数。
每当线程想要进入一个被保护部分的时候,它将会调用信号量的wait()
。信号量将会减小它的计数,只要计数不为0,线程就能够继续。反之,它会将线程存储在它的队列里。当被保护部分离开一个线程的时候,它会调用signal()
来通知信号量。信号量首先会检查,是否在队列中有等待的线程,若是有,它将唤醒线程,让它继续。若是没有,它将会再次增长它的计数。
在 iOS 中,咱们可使用 DispatchSemaphors 来实现这种行为。比起默认信号量,它更倾向于使用 DispatchSemaphors ,由于它们只在真正须要时才会降低到内核级别。不然,它的运行速度会快得多。
let s = DispatchSemaphore(value: 1)
_ = s.wait(timeout: DispatchTime.distantFuture)
// do something
s.signal()
复制代码
有人认为二进制的信号量(计数为1的信号量)和互斥是同样的。但互斥是一种锁的机制,信号量是一种信号的机制。这个解释并无什么帮助,因此它们到底有什么不一样呢?
锁机制是关于保护和管理一个资源的访问,因此它会阻止多个线程同时访问一个资源。信号系统更像是"Hey 我完事了,继续!"。举个栗子:若是你拿你的手机正在听歌,这时候来了一个电话。当你通完电话,它将会给你的 player 发送一个通知让它继续。这是一个在互斥上考虑信号量的状况。
译者注:我猜想,放歌和听音乐是互斥的,由于你不可能接电话的时候还听着歌。在通话完成后,手机给player发送一个信号让它继续放歌,这是一个信号量的操做。
假如你有一个低优先级的线程1在受保护的区域,你还有一个高优先级的线程2被信号量调用wait()
使其等待。此时线程2处于休眠状态等待信号量将其唤醒。此时,咱们有一个线程3,优先级高于线程1。线程3会联合 Qos 来阻止线程1去通知信号量,所以而覆盖其余线程。因此 iOS 中的信号量并无优先级继承。
在 OC 中,有一个@synchronized
的关键字。这是建立互斥的简单方法。因为 Swift 不支持,咱们不得不用更底层的方法:objc_sync_enter
。
let lock = self
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
复制代码
由于我在网上看到这个问题不少次,因此让咱们回答一下。据我所知,这不是一个私有方法,因此使用它不会被 App Store 拒审。
因为 Swift 中没有 metux
,并且 synchornized
也被移除,因此使用 DispatchQueues
成了 Swift 开发者的黄金法则。当用它实现同步的时候,它与 metux
有相同的行为。由于全部操做都在同一队列中排队。这能够防止同时执行。
它的缺点是它的时间消耗大,它必须常常分配和改变上下文。若是你的 App 不须要任何高计算能力,这就可有可无了。可是若是遇到帧丢失等问题,你可能就须要考虑别的方案了(例如 Mutex)。
若是你使用 GCD,你有不少办法来同步代码。其中一个就是 Dispatch Barriers。经过它,咱们能够建立须要一块儿执行的被保护部分的 block 。咱们也能够异步执行这些代码,这听起来很奇怪,可是假想一下,你有一个耗时的操做,它能够被分割为几个小任务。这些小任务能够被异步执行,当小任务都执行完,Dispatch Barriers 在去同步这些小任务。
译者注:好比将一张大图分割为几张小图异步下载,等小图都异步下载完,再同步为一张大图。
它并非操做系统提供的一种机制。它是一种模式:用来确保方法在正确的线程被调用。它的思路很简单,在开始检查方法是否在正确的线程上,若是不在,它会在正确的线程上调用它本身并返回。有时,你须要使用上面锁的机制来实现等待程序。只有在调用方法有返回值的时候才会发生这种状况。不然,你能够简单的返回。
func executeOnMain() {
if !Thread.isMainThread {
DispatchQueue.main.async(execute: {() -> Void in
executeOnMain()
return
})
}
// do something
}
复制代码
不要常用这种模式。虽然它能够确保你在正确的线程,但同时,它会使你的同事困惑。他们可能不理解你处处改变线程。某些时候,它会使你的代码 like shit
,而且浪费你的时间去整理代码。
Wow,这真是一篇工做量很大的文章。这里有如此多的技术能够实现并发编程,这篇文章只是浅尝辄止。当我在讨论并发的时候,你们都厌烦我,可是并发编程真的很重要,同事们也在慢慢的承认我。今天,我不得不修复一个数组异步的问题,咱们知道 Swift 不支持原子性操做。你猜怎么着?这形成了一个崩溃。若是咱们更加了解并发,可能就不会出现这个问题。但说实话,我以前也不知道这个。
我能给你最好的建议就是:知己知彼,百战不殆。综合上文所述,我但愿你能够开始学习并发,能找到一种方法用来解决遇到的问题。一旦你更加深刻,你就会愈来愈明了。Good Luck!