iOS多线程编程——GCD与NSOperation总结

Posted WeiAreYoung

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS多线程编程——GCD与NSOperation总结相关的知识,希望对你有一定的参考价值。

很长时间以来,我个人(可能还有很多同学),对多线程编程都存在一些误解。一个很明显的表现是,很多人有这样的看法:

新开一个线程,能提高速度,避免阻塞主线程

毕竟多线程嘛,几个线程一起跑任务,速度快,还不阻塞主线程,简直完美。

在某些场合,我们还见过另一个“高深”的名词——“异步”。这东西好像和多线程挺类似,经过一番百度(阅读了很多质量层次不齐的文章)之后,很多人也没能真正搞懂何为“异步”。

于是,带着对“多线程”和“异步”的懵懂,很多人又开开心心踏上了多线程编程之旅,比如文章待会儿会提到的GCD。

何为多线程

其实,如果不考虑其他任何因素和技术,多线程有百害而无一利,只能浪费时间,降低程序效率。

是的,我很清醒的写下这句话。

试想一下,一个任务由十个子任务组成。现在有两种方式完成这个任务: 1. 建十个线程,把每个子任务放在对应的线程中执行。执行完一个线程中的任务就切换到另一个线程。 
2. 把十个任务放在一个线程里,按顺序执行。

操作系统的基础知识告诉我们,线程,是执行程序最基本的单元,它有自己栈和寄存器。说得再具体一些,线程就是“一个CPU执行的一条无分叉的命令列”

对于第一种方法,在十个线程之间来回切换,就意味着有十组栈和寄存器中的值需要不断地被备份、替换。 而对于对于第二种方法,只有一组寄存器和栈存在,显然效率完胜前者。

并发与并行

通过刚刚的分析我们看到,多线程本身会带来效率上的损失。准确来说,在处理并发任务时,多线程不仅不能提高效率,反而还会降低程序效率。

所谓的“并发”,英文翻译是concurrent。要注意和“并行(parallelism)”的区别。

并发指的是一种现象,一种经常出现,无可避免的现象。它描述的是“多个任务同时发生,需要被处理”这一现象。它的侧重点在于“发生”。

比如有很多人排队等待检票,这一现象就可以理解为并发。

并行指的是一种技术,一个同时处理多个任务的技术。它描述了一种能够同时处理多个任务的能力,侧重点在于“运行”。

比如景点开放了多个检票窗口,同一时间内能服务多个游客。这种情况可以理解为并行。

并行的反义词就是串行,表示任务必须按顺序来,一个一个执行,前一个执行完了才能执行后一个。

我们经常挂在嘴边的“多线程”,正是采用了并行技术,从而提高了执行效率。因为有多个线程,所以计算机的多个CPU可以同时工作,同时处理不同线程内的指令。

并发是一种现象,面对这一现象,我们首先创建多个线程,真正加快程序运行速度的,是并行技术。也就是让多个CPU同时工作。而多线程,是为了让多个CPU同时工作成为可能。

同步与异步

同步方法就是我们平时调用的哪些方法。因为任何有编程经验的人都知道,比如在第一行调用foo()方法,那么程序运行到第二行的时候,foo方法肯定是执行完了。

所谓的异步,就是允许在执行某一个任务时,函数立刻返回,但是真正要执行的任务稍后完成。

比如我们在点击保存按钮之后,要先把数据写到磁盘,然后更新UI。同步方法就是等到数据保存完再更新UI,而异步则是立刻从保存数据的方法返回并向后执行代码,同时真正用来保存数据的指令将在稍后执行。

区别和联系

假设现在有三个任务需要处理。假设单个CPU处理它们分别需要3、1、1秒。

并行与串行,其实讨论的是处理这三个任务的速度问题。如果三个CPU并行处理,那么一共只需要3秒。相比于串行处理,节约了两秒。

而同步/异步,其实描述的是任务之间先后顺序问题。假设需要三秒的那个是保存数据的任务,而另外两个是UI相关的任务。那么通过异步执行第一个任务,我们省去了三秒钟的卡顿时间。

对于同步执行的三个任务来说,系统倾向于在同一个线程里执行它们。因为即使开了三个线程,也得等他们分别在各自的线程中完成。并不能减少总的处理时间,反而徒增了线程切换(这就是文章开头举的例子)

对于异步执行的三个任务来说,系统倾向于在三个新的线程里执行他们。因为这样可以最大程度的利用CPU性能,提升程序运行效率。

总结

于是我们可以得出结论,在需要同时处理IO和UI的情况下,真正起作用的是异步,而不是多线程。可以不用多线程(因为处理UI非常快),但不能不用异步(否则的话至少要等IO结束)。

注意到我把“倾向于”这三个加粗了,也就是说异步方法并不一定永远在新线程里面执行,反之亦然。在接下来关于GCD的部分会对此做出解释。

GCD简介

GCD以block为基本单位,一个block中的代码可以为一个任务。下文中提到任务,可以理解为执行某个block

同时,GCD中有两大最重要的概念,分别是“队列”和“执行方式”。

使用block的过程,概括来说就是把block放进合适的队列,并选择合适的执行方式去执行block的过程。

三种队列:

  1. 串行队列(先进入队列的任务先出队列,每次只执行一个任务) 
  2. 并发队列(依然是“先入先出”,不过可以形成多个任务并发) 
  3. 主队列(这是一个特殊的串行队列,而且队列中的任务一定会在主线程中执行)

两种执行方式:

  1. 同步执行 
  2. 异步执行

关于同步异步、串行并行和线程的关系,下面通过一个表格来总结

可以看到,同步方法不一定在本线程,异步方法方法也不一定新开线程(考虑主队列)。

然而事实上,在本文一开始就揭开了“多线程”的神秘面纱,所以我们在编程时,更应该考虑的是:

同步 Or 异步

以及

串行 Or 并行

而非仅仅考虑是否新开线程。

当然,了解任务运行在那个线程中也是为了更加深入的理解整个程序的运行情况,尤其是接下来要讨论的死锁问题。

GCD的死锁问题

在使用GCD的过程中,如果向当前串行队列中同步派发一个任务,就会导致死锁。

这句话有点绕,我们首先举个例子看看:

override func viewDidLoad()   
    super.viewDidLoad()
    let mainQueue = dispatch_get_main_queue()
    let block =  ()  in
        print(NSThread.currentThread())
        
    dispatch_sync(mainQueue, block)

这段代码就会导致死锁,因为我们目前在主队列中,又将要同步地添加一个block到主队列(串行)中。

理论分析

我们知道dispatch_sync表示同步的执行任务,也就是说执行dispatch_sync后,当前队列会阻塞。而dispatch_sync中的block如果要在当前队列中执行,就得等待当前队列程执行完成。

在上面这个例子中,主队列在执行dispatch_sync,随后队列中新增一个任务block。因为主队列是同步队列,所以block要等dispatch_sync执行完才能执行,但是dispatch_sync是同步派发,要等block执行完才算是结束。在主队列中的两个任务互相等待,导致了死锁。

解决方案

其实在通常情况下我们不必要用dispatch_sync,因为dispatch_async能够更好的利用CPU,提升程序运行速度。

只有当我们需要保证队列中的任务必须顺序执行时,才考虑使用dispatch_sync。在使用dispatch_sync的时候应该分析当前处于哪个队列,以及任务会提交到哪个队列。

GCD任务组

了解完队列之后,很自然的会有一个想法:我们怎么知道所有任务都已经执行完了呢?

在单个串行队列中,这个不是问题,因为只要把回调block添加到队列末尾即可。

但是对于并行队列,以及多个串行、并行队列混合的情况,就需要使用 dispatch_group 了。

let group = dispatch_group_create()

dispatch_group_async(group, serialQueue,  () -> Void in  
    for _ in 0..<2 
        print("group-serial \\(NSThread.currentThread())")
    
)

dispatch_group_async(group, serialQueue,  () -> Void in  
    for _ in 0..<3 
        NSLog("group-02 - %@", NSThread.currentThread())
    
)

dispatch_group_notify(group, serialQueue,  () -> Void in  
    print("完成 - \\(NSThread.currentThread())")
)

首先我们要通过 dispatch_group_create() 方法生成一个组。

接下来,我们把 dispatch_async 方法换成 dispatch_group_async。这个方法多了一个参数,第一个参数填刚刚创建的分组。

想问 dispatch_sync 对应的分组方法是什么的童鞋面壁思过三秒钟,思考一下 group 出现的目的和 dispatch_sync 的特点。

最后调用 dispatch_group_notify 方法。这个方法表示把第三个参数 block 传入第二个参数队列中去。而且可以保证第三个参数 block 执行时,group 中的所有任务已经全部完成。

dispatch_group

dispatch_group_wait 方法是一个很有用的方法,它的完整定义如下:

dispatch_group_wait(group: dispatch_group_t, _ timeout: dispatch_time_t) -> Int

第一个参数表示要等待的 group,第二个则表示等待时间。返回值表示经过指定的等待时间,属于这个 group 的任务是否已经全部执行完,如果是则返回 0,否则返回非 0。

第二个 dispatch_time_t 类型的参数还有两个特殊值:DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER

前者表示立刻检查属于这个 group 的任务是否已经完成,后者则表示一直等到属于这个 group 的任务全部完成。

dispatch_after方法

通过 GCD 还可以进行简单的定时操作,比如在 1 秒后执行某个 block 。代码如下:

let mainQueue = dispatch_get_main_queue()  
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(3) * Int64(NSEC_PER_SEC))  
NSLog("%@",NSThread.currentThread())  
dispatch_after(time, mainQueue, () in NSLog("%@",NSThread.currentThread()))  

dispatch_after 方法有三个参数。第一个表示时间,也就是从现在起往后三秒钟。第二、三个参数分别表示要提交的任务和提交到哪个队列

以上是关于iOS多线程编程——GCD与NSOperation总结的主要内容,如果未能解决你的问题,请参考以下文章

iOS多线程编程——GCD与NSOperation总结

iOS多线程——GCD与NSOperation总结

iOS并发编程对比总结,NSThread,NSOperation,GCD - iOS

iOS多线程全套:线程生命周期,多线程的四种解决方案,线程安全问题,GCD的使用,NSOperation的使用

iOS多线程全套:线程生命周期,多线程的四种解决方案,线程安全问题,GCD的使用,NSOperation的使用(上)

iOS多线程总结——NSOperation与NSOperationQueue的使用