在初学iOS相关知识过程当中,大多都对多线程有些恐惧的内心,同时感受工做中用上的几率不大。可是若是平时很少积累并学透多线程,当工做中真的须要用到的时候,就极可能简单百度后把一些知识点稀里糊涂地就用到工做中了,却不知里面有不少的坑,也有不少技巧须要在理论上先作了解,再结合实战,进一步去体会多线程的魅力和强大。html
接下来,就对多线程来源的背景进行简单的介绍:ios
在计算的早期,计算机能够执行的最大工做量是由 CPU 的时钟速度决定的。可是随着技术的进步和处理器设计的紧凑化,热量和其余物理约束开始限制处理器的最大时钟速度。所以,芯片制造商寻找其余方法来提升芯片的整体性能。他们决定的解决方案是增长每一个芯片上的处理器核心数量。经过增长内核的数量,一个单独的芯片能够每秒执行更多的指令,而不用增长 CPU 的速度或改变芯片的大小或热特性。惟一的问题是如何利用额外的内核。git
应用程序使用多核的传统方法是建立多个线程。与依赖线程不一样,iOS 采用异步设计方法来解决并发问题。一般,这项工做涉及获取一个后台线程,在该线程上启动所需的任务,而后在任务完成时向调用方发送通知(一般经过一个回调函数)。github
iOS 提供了一些技术,容许您异步执行任何任务,而无需本身管理线程。异步启动任务的技术之一是 Grand Central Dispatch (GCD)。这种技术采用线程管理代码,并将该代码移动到系统级别。您所要作的就是定义要执行的任务,并将它们添加到适当的分派队列中。GCD 负责建立所需的线程,并安排任务在这些线程上运行。因为线程管理如今是系统的一部分,GCD 提供了任务管理和执行的总体方法,比传统线程提供了更高的效率。编程
OperationQueue(操做队列,api 类名为 NSOperationQueue )是 Objective-C 对象,是对 GCD 的封装。其做用很是相似于分派队列。您定义要执行的任务,而后将它们添加到 OperationQueue 中, OperationQueue 处理这些任务的调度和执行。与 GCD 同样, OperationQueue 为您处理全部线程管理,确保在系统上尽量快速有效地执行任务。swift
接下来,就对如今工做中经常使用的这两种技术进行比较和实例解析。api
GCD:安全
OperationQueue:网络
OC 框架,更加面向对象,是对 GCD 的封装。数据结构
iOS 2.0 推出的,苹果推出 GCD 以后,对 NSOperation 的底层进行了所有重写。
能够设置队列中每个操做的 QOS() 队列的总体 QOS
操做相关
Operation做为一个对象,为咱们提供了更多的选择:
任务依赖(addDependency),能够跨队列设置操做的依赖关系;
在队列中的优先级(queuePriority)
服务质量(qualityOfService, iOS8+);
完成回调(void (^completionBlock)(void)
队列相关
服务质量(qualityOfService, iOS8+);
最大并发操做数(maxConcurrentOperationCount),GCD 不易实现;
暂停/继续(suspended);
取消全部操做(cancelAllOperations);
KVO 监听队列任务执行进度(progress, iOS13+);
接下来经过文字,结合实践代码(工程连接在文末)和运行效果 gif 图对部分功能进行分析。
串行队列中的任务按顺序执行;可是不一样串行队列间没有任何约束; 多个串行队列同时执行时,不一样队列中任务执行是并发的效果。好比:火车站买票能够有多个卖票口,可是每一个排的队都是串行队列,总体并发,单线串行。
注意防坑:串行队列建立的位置。好比下面代码示例中:在for循环内部建立时,每一个循环都是建立一个新的串行队列,里面只装一个任务,多个串行队列,结果总体上是并发的效果。想要串行效果,必须在for循环外部建立串行队列。
串行队列适合管理共享资源。保证了顺序访问,杜绝了资源竞争。
代码示例:
private func serialExcuteByGCD(){ let lArr : [UIImageView] = [imageView1, imageView2, imageView3, imageView4] //串行队列,异步执行时,只开一个子线程 let serialQ = DispatchQueue.init(label: "com.companyName.serial.downImage") for i in 0..<lArr.count{ let lImgV = lArr[i] //清空旧图片 lImgV.image = nil //注意,防坑:串行队列建立的位置,在这建立时,每一个循环都是一个新的串行队列,里面只装一个任务,多个串行队列,总体上是并行的效果。 // let serialQ = DispatchQueue.init(label: "com.companyName.serial.downImage") serialQ.async { print("第\(i)个 开始,%@",Thread.current) Downloader.downloadImageWithURLStr(urlStr: imageURLs[i]) { (img) in let lImgV = lArr[i] print("第\(i)个 结束") DispatchQueue.main.async { print("第\(i)个 切到主线程更新图片") lImgV.image = img } if nil == img{ print("第\(i+1)个img is nil") } } } } }
gif 效果图:
图中下载时可顺利拖动滚动条,是为了说明下载在子线程,不影响UI交互
log:
第0个 开始 第0个 结束 第1个 开始 第0个 更新图片 第1个 结束 第2个 开始 第1个 更新图片 第2个 结束 第3个 开始 第2个 更新图片 第3个 结束 第3个 更新图片
由 log 可知: GCD 切到主线程也须要时间,切换完成以前,指令可能已经执行到下个循环了。可是看起来图片仍是依次下载完成和显示的,由于每一张图切到主线程显示都须要时间。
并发队列依旧保证中任务按加入的前后顺序开始(FIFO),可是没法知道执行顺序,执行时长和某一时刻的任务数。按 FIFO 开始后,他们之间不会相互等待。
好比:提交了 #1,#2,#3 任务到并发队列,开始的顺序是 #1,#2,#3。#2 和 #3 虽然开始的比 #1 晚,可是可能比 #1 执行结束的还要早。任务的执行是由系统决定的,因此执行时长和结束时间都没法肯定。
须要用到并发队列时,强烈建议 使用系统自带的四种全局队列之一。可是,当你须要使用 barrier 对队列中任务进行栅栏时,只能使用自定义并发队列。
Use a barrier to synchronize the execution of one or more tasks in your dispatch queue. When you add a barrier to a concurrent dispatch queue, the queue delays the execution of the barrier block (and any tasks submitted after the barrier) until all previously submitted tasks finish executing. After the previous tasks finish executing, the queue executes the barrier block by itself. Once the barrier block finishes, the queue resumes its normal execution behavior.
对比:barrier 和锁的区别
代码示例:
private func concurrentExcuteByGCD(){ let lArr : [UIImageView] = [imageView1, imageView2, imageView3, imageView4] for i in 0..<lArr.count{ let lImgV = lArr[i] //清空旧图片 lImgV.image = nil //并行队列:图片下载任务按顺序开始,可是是并行执行,不会相互等待,任务结束和图片显示顺序是无序的,多个子线程同时执行,性能更佳。 let lConQ = DispatchQueue.init(label: "cusQueue", qos: .background, attributes: .concurrent) lConQ.async { print("第\(i)个开始,%@", Thread.current) Downloader.downloadImageWithURLStr(urlStr: imageURLs[i]) { (img) in let lImgV = lArr[i] print("第\(i)个结束") DispatchQueue.main.async { lImgV.image = img } if nil == img{ print("第\(i+1)个img is nil") } } } } }
gif 效果图:
log:
第0个开始,%@ <NSThread: 0x600002de2e00>{number = 4, name = (null)} 第1个开始,%@ <NSThread: 0x600002dc65c0>{number = 6, name = (null)} 第2个开始,%@ <NSThread: 0x600002ddc8c0>{number = 8, name = (null)} 第3个开始,%@ <NSThread: 0x600002d0c8c0>{number = 7, name = (null)} 第0个结束 第3个结束 第1个结束 第2个结束
/** Submits a block for asynchronous execution on a main queue and returns immediately. */ static inline void dispatch_async_on_main_queue(void (^block)()) { if (NSThread.isMainThread) { block(); } else { dispatch_async(dispatch_get_main_queue(), block); } }
主队列是串行队列,每一个时间点只能有一个任务执行,所以若是耗时操做放到主队列,会致使界面卡顿。
系统提供一个串行主队列,4个 不一样优先级的全局队列。
用 dispatch_get_global_queue 方法获取全局队列时,第一个参数有 4 种类型可选:
DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND
串行队列异步执行时,切到主线程刷 UI 也须要时间,切换完成以前,指令可能已经执行到下个循环了。可是看起来图片仍是依次下载完成和显示的,由于每一张图切到主线程显示都须要时间。详见 demo 示例。
iOS8 以后,若是须要添加可被取消的任务,可使用 DispatchWorkItem 类,此类有 cancel 方法。
应该避免建立大量的串行队列,若是但愿并发执行大量任务,请将它们提交给全局并发队列之一。建立串行队列时,请尝试为每一个队列肯定一个用途,例如保护资源或同步应用程序的某些关键行为(如蓝牙检测结果须要有序处理的逻辑)。
调度队列复制添加到它们中的块,并在执行完成时释放块。
虽然队列在执行小任务时比原始线程更有效,可是建立块并在队列上执行它们仍然存在开销。若是一个块执行的工做量太少,那么内联执行它可能比将它分派到队列中要便宜得多。判断一个块是否工做量太少的方法是使用性能工具为每一个路径收集度量数据并进行比较。
您可能但愿将 block 的部分代码包含在 @autoreleasepool 中,以处理这些对象的内存管理。尽管 GCD 调度队列拥有本身的自动释放池,但它们不能保证这些池什么时候耗尽。若是您的应用程序是内存受限的,那么建立您本身的自动释放池可让您以更有规律的间隔释放自动释放对象的内存。
dispatch_after 函数并非在指定时间以后才开始执行处理,而是在指定时间以后将任务追加到队列中。这个时间并非绝对准确的。
代码示例:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"2s后执行"); });
在多线程访问可变变量时,是非线程安全的。可能致使程序崩溃。此时,能够经过使用信号量(semaphore)技术,保证多线程处理某段代码时,后面线程等待前面线程执行,保证了多线程的安全性。使用方法记两个就好了,一个是wait(dispatch_semaphore_wait),一个是signal(dispatch_semaphore_signal)。
具体请参考文章Semaphore回顾
当每次迭代中执行工做与其余全部迭代中执行的工做不一样,且每一个循环完成的顺序不重要时,能够用 dispatch_apply 函数替换循环。注意:替换后, dispatch_apply 函数总体上是同步执行,内部 block 的执行类型(串行/并发)由队列类型决定,可是串行队列易死锁,建议用并发队列。
原循环:
for (i = 0; i < count; i++) { printf("%u\n",i); } printf("done");
优化后:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //count 是迭代的总次数。 dispatch_apply(count, queue, ^(size_t i) { printf("%u\n",i); }); //一样在上面循环结束后才调用。 printf("done");
您应该确保您的任务代码在每次迭代中完成合理数量的工做。与您分派到队列的任何块或函数同样,调度该代码以便执行会带来开销。若是循环的每次迭代只执行少许的工做,那么调度代码的开销可能会超过将代码分派到队列可能带来的性能优点。若是您在测试期间发现这一点是正确的,那么您可使用步进来增长每一个循环迭代期间执行的工做量。经过大步前进,您能够将原始循环的多个迭代集中到一个块中,并按比例减小迭代次数。例如,若是您最初执行了 100次 迭代,但决定使用步长为 4 的迭代,那么您如今从每一个块执行 4 次循环迭代,迭代次数为 25次 。
一个队列的不一样任务能够在多个线程执行吗?
答:串行队列,异步执行时,只开一个子线程;无所谓多个线程执行;
并发队列,异步执行时,会自动开多个线程,能够在多个线程并发执行不一样的任务。
一个线程能够同时执行多个队列的任务吗?
答:一个线程某个时间点只能执行一个任务,执行完毕后,可能执行到来自其余队列的任务(若是有的话)。好比:主线程除了执行主队列中任务外,也可能会执行非主队列中的任务。
队列与线程关系示例图:
qualityOfService 和 queuePriority 的区别是什么?
答:
qualityOfService:
用于表示 operation 在获取系统资源时的优先级,默认值:NSQualityOfServiceBackground,咱们能够根据须要给 operation 赋不一样的优化级,如最高优化级:NSQualityOfServiceUserInteractive。
queuePriority:
用于设置 operation 在 operationQueue 中的相对优化级,同一 queue 中优化级高的 operation(isReady 为 YES) 会被优先执行。
须要注意区分 qualityOfService (在系统层面,operation 与其余线程获取资源的优先级) 与 queuePriority (同一 queue 中 operation 间执行的优化级)的区别。同时,须要注意 dependencies (严格控制执行顺序)与 queuePriority (queue 内部相对优先级)的区别。
添加依赖后,队列中网络请求任务有依赖关系时,任务结束断定以数据返回为准仍是以发起请求为准?
答:以发起请求为准。分析过程详见NSOperationQueue队列中操做依赖相关思考
NSOperation
NSOperation 是一个"抽象类",不能直接使用。抽象类的用处是定义子类共有的属性和方法。NSOperation 是基于 GCD 作的面向对象的封装。相比较 GCD 使用更加简单,而且提供了一些用 GCD 不是很好实现的功能。是苹果公司推荐使用的并发技术。它有两个子类:
NSOperationQueue
OperationQueue也是对 GCD 的高级封装,更加面向对象,能够实现 GCD 不方便实现的一些效果。被添加到队列的操做默认是异步执行的。
PS:常见的抽象类有:
经过对不一样操做设置依赖,或优先级,可实现 非FIFO 效果。
代码示例:
func testDepedence(){ let op0 = BlockOperation.init { print("op0") } let op1 = BlockOperation.init { print("op1") } let op2 = BlockOperation.init { print("op2") } let op3 = BlockOperation.init { print("op3") } let op4 = BlockOperation.init { print("op4") } op0.addDependency(op1) op1.addDependency(op2) op0.queuePriority = .veryHigh op1.queuePriority = .normal op2.queuePriority = .veryLow op3.queuePriority = .low op4.queuePriority = .veryHigh gOpeQueue.addOperations([op0, op1, op2, op3, op4], waitUntilFinished: false) }
log:
op4 op2 op3 op1 op0
或
op4 op3 op2 op1 op0
说明:操做间不存在依赖时,按优先级执行;存在依赖时,按依赖关系前后执行(与无依赖关系的其余任务相比,依赖集合的执行顺序不肯定)
经过对队列的isSuspended
属性赋值,可实现队列中未执行任务的暂停和继续效果。正在执行的任务不受影响。
///暂停队列,只对未执行中的任务有效。本例中对串行队列的效果明显。并发队列因4个任务一开始就很容易一块儿开始执行,即便挂起也没法影响已处于执行状态的任务。 @IBAction func pauseQueueItemDC(_ sender: Any) { gOpeQueue.isSuspended = true } ///恢复队列,以前未开始执行的任务会开始执行 @IBAction func resumeQueueItemDC(_ sender: Any) { gOpeQueue.isSuspended = false }
gif 效果图:
取消(cancel)时,有 3 种状况:
1.操做在队列中等待执行,这种状况下,操做将不会被执行。
2.操做已经在执行中,此时,系统不会强制中止这个操做,可是,其 cancelled
属性会被置为 true 。
3.操做已完成,此时,cancel 无任何影响。
方法: cancelAllOperations。一样只会对未执行的任务有效。
demo 中代码:
deinit { gOpeQueue.cancelAllOperations() print("die:%@",self) }
经过设置操做间依赖,能够实现 非FIFO 的指定顺序效果。那么,经过设置最大并发数为 1 ,能够实现指定顺序效果吗?
A:不能够!
设置最大并发数为 1 后,虽然每一个时间点只执行一个操做,可是操做的执行顺序仍然基于其余因素,如操做的依赖关系,操做的优先级(依赖关系比优先级级别更高,即先根据依赖关系排序;不存在依赖关系时,才根据优先级排序)。所以,序列化 操做队列 不会提供与 GCD 中的序列 分派队列 彻底相同的行为。若是操做对象的执行顺序对您很重要,那么您应该在将操做添加到队列以前使用 依赖关系 创建该顺序,或改用 GCD 的 串行队列 实现序列化效果。
Operation Queue的 block 中为什么无需使用 [weak self] 或 [unowned self] ?
A:即便队列对象是为全局的,self -> queue -> operation block -> self,的确会形成循环引用。可是在队列里的操做执行完毕时,队列会自动释放操做,自动解除循环引用。因此没必要使用 [weak self] 或 [unowned self] 。
此外,这种循环引用在某些状况下很是有用,你无需额外持有任何对象就可让操做自动完成它的任务。好比下载页面下载过程当中,退出有循环引用的界面时,若是不执行 cancelAllOperation 方法,能够实现继续执行剩余队列中下载任务的效果。
func addOperation(_ op: Operation)
Discussion:
Once added, the specified operation remains in the queue until it finishes executing.
Declaration
func addOperation(_ block: @escaping () -> Void)
Parameters
block
The block to execute from the operation. The block takes no parameters and has no return value.
Discussion
This method adds a single block to the receiver by first wrapping it in an operation object. You should not attempt to get a reference to the newly created operation object or determine its type information.
This property specifies the service level applied to operation objects added to the queue. If the operation object has an explicit service level set, that value is used instead.
资源竞争可能致使数据异常,死锁,甚至因访问野指针而崩溃。
func testDeadLock(){ //主队列同步执行,会致使死锁。block须要等待testDeadLock执行,而主队列同步调用,又使其余任务必须等待此block执行。因而造成了相互等待,就死锁了。 DispatchQueue.main.sync { print("main block") } print("2") }
可是下面代码不会死锁,故串行队列同步执行任务不必定死锁。
- (void)testSynSerialQueue{ dispatch_queue_t myCustomQueue; myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL); dispatch_async(myCustomQueue, ^{ printf("Do some work here.\n"); }); printf("The first block may or may not have run.\n"); dispatch_sync(myCustomQueue, ^{ printf("Do some more work here.\n"); }); printf("Both blocks have completed.\n"); }
代码设计优先级:系统方法 > 并行 > 串行 > 锁,简记为:西饼传说
Concurrency Programming Guide
iOS Concurrency: Getting Started with NSOperation and Dispatch Queues
文中提到的知识点,“与其用操做对象淹没队列,不如批量建立这些对象。当一个批处理完成执行时,使用完成块告诉应用程序建立一个新的批处理”,在最近的工做中的确有须要相似的需求,等有时间会进行总结,就做为下一篇文章的预告吧。
本文由博客群发一文多发等运营工具平台 OpenWrite 发布