目录 背景简介在初学iOS相关知识过程中,大多都对多线程有些恐惧的心里,同时感觉工作中用上的概率不大。但是如果平时不多积累并学透多线程,当工作中真的需要用到的时候,就很可能简单百度后把一些知识点稀里糊涂地就用到工作中了,殊不知里面有很多的坑,也有很多技巧需要在理论上先做了解,再结合实战,进一步去体会多线程的魅力和强大。 接下来,就对多线程来源的背景进行简单的介绍: 在计算的早期,计算机可以执行的最大工作量是由 CPU 的时钟速度决定的。但是随着技术的进步和处理器设计的紧凑化,热量和其他物理约束开始限制处理器的最大时钟速度。因此,芯片制造商寻找其他方法来提高芯片的总体性能。他们决定的解决方案是增加每个芯片上的处理器核心数量。通过增加内核的数量,一个单独的芯片可以每秒执行更多的指令,而不用增加 CPU 的速度或改变芯片的大小或热特性。唯一的问题是如何利用额外的内核。 应用程序使用多核的传统方法是创建多个线程。与依赖线程不同,iOS 采用异步设计方法来解决并发问题。通常,这项工作涉及获取一个后台线程,在该线程上启动所需的任务,然后在任务完成时向调用方发送通知(通常通过一个回调函数)。 iOS 提供了一些技术,允许您异步执行任何任务,而无需自己管理线程。异步启动任务的技术之一是 Grand Central Dispatch (GCD)。这种技术采用线程管理代码,并将该代码移动到系统级别。您所要做的就是定义要执行的任务,并将它们添加到适当的分派队列中。GCD 负责创建所需的线程,并安排任务在这些线程上运行。由于线程管理现在是系统的一部分,GCD 提供了任务管理和执行的整体方法,比传统线程提供了更高的效率。 OperationQueue(操作队列,api 类名为 NSOperationQueue )是 Objective-C 对象,是对 GCD 的封装。其作用非常类似于分派队列。您定义要执行的任务,然后将它们添加到 OperationQueue 中, OperationQueue 处理这些任务的调度和执行。与 GCD 一样, OperationQueue 为您处理所有线程管理,确保在系统上尽可能快速有效地执行任务。 接下来,就对现在工作中常用的这两种技术进行比较和实例解析。 GCD、OperationQueue 对比核心理念
区别
接下来通过文字,结合实践代码(工程链接在文末)和运行效果 gif 图对部分功能进行分析。 GCD队列串行队列(Serial Queues)串行队列中的任务按顺序执行;但是不同串行队列间没有任何约束; 多个串行队列同时执行时,不同队列中任务执行是并发的效果。比如:火车站买票可以有多个卖票口,但是每个排的队都是串行队列,整体并发,单线串行。 注意防坑:串行队列创建的位置。比如下面代码示例中:在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 效果图:
log: 第0个 开始 第0个 结束 第1个 开始 第0个 更新图片 第1个 结束 第2个 开始 第1个 更新图片 第2个 结束 第3个 开始 第2个 更新图片 第3个 结束 第3个 更新图片 由 log 可知: GCD 切到主线程也需要时间,切换完成之前,指令可能已经执行到下个循环了。但是看起来图片还是依次下载完成和显示的,因为每一张图切到主线程显示都需要时间。 并发队列(Concurrent Queues)并发队列依旧保证中任务按加入的先后顺序开始(FIFO),但是无法知道执行顺序,执行时长和某一时刻的任务数。按 FIFO 开始后,他们之间不会相互等待。 比如:提交了 #1,#2,#3 任务到并发队列,开始的顺序是 #1,#2,#3。#2 和 #3 虽然开始的比 #1 晚,但是可能比 #1 执行结束的还要早。任务的执行是由系统决定的,所以执行时长和结束时间都无法确定。 需要用到并发队列时,强烈建议 使用系统自带的四种全局队列之一。但是,当你需要使用 barrier 对队列中任务进行栅栏时,只能使用自定义并发队列。
对比: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); } }
block(块)相关 调度队列复制添加到它们中的块,并在执行完成时释放块。 dispatch_after dispatch_after 函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到队列中。这个时间并不是绝对准确的。 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"2s后执行"); }); dispatch_semaphore在多线程访问可变变量时,是非线程安全的。可能导致程序崩溃。此时,可以通过使用信号量(semaphore)技术,保证多线程处理某段代码时,后面线程等待前面线程执行,保证了多线程的安全性。使用方法记两个就行了,一个是wait(dispatch_semaphore_wait),一个是signal(dispatch_semaphore_signal)。
dispatch_apply当每次迭代中执行工作与其他所有迭代中执行的工作不同,且每个循环完成的顺序不重要时,可以用 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次 。 自问自答
OperationQueue
PS:常见的抽象类有:
可以实现 非FIFO 效果通过对不同操作设置依赖,或优先级,可实现 非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 说明:操作间不存在依赖时,按优先级执行;存在依赖时,按依赖关系先后执行(与无依赖关系的其他任务相比,依赖集合的执行顺序不确定) 队列暂停/继续通过对队列的 ///暂停队列,只对未执行中的任务有效。本例中对串行队列的效果明显。并发队列因4个任务一开始就很容易一起开始执行,即使挂起也无法影响已处于执行状态的任务。 @IBAction func pauseQueueItemDC(_ sender: Any) { gOpeQueue.isSuspended = true } ///恢复队列,之前未开始执行的任务会开始执行 @IBAction func resumeQueueItemDC(_ sender: Any) { gOpeQueue.isSuspended = false } gif 效果图: 取消操作
取消单个操作对象取消(cancel)时,有 3 种情况: 取消队列中的所有操作对象方法: cancelAllOperations。同样只会对未执行的任务有效。 deinit { gOpeQueue.cancelAllOperations() print("die:%@",self) } 自问自答
常见问题如何解决资源竞争问题资源竞争可能导致数据异常,死锁,甚至因访问野指针而崩溃。
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"); } 如何提高代码效率“西饼传说”代码设计优先级:系统方法 > 并行 > 串行 > 锁,简记为:西饼传说
确定操作对象的适当范围
术语解释摘录
本文 demo 地址参考文章
下节预告文中提到的知识点,“与其用操作对象淹没队列,不如批量创建这些对象。当一个批处理完成执行时,使用完成块告诉应用程序创建一个新的批处理”,在最近的工作中的确有需要类似的需求,等有时间会进行总结,就作为下一篇文章的预告吧。
|
|