今天来看一下iOS中的多线程开发,iOS的多线程开发也有很多种方式,不得不提的有一个就是GCD了。在介绍他之前,先放上苹果官方的GCD源码。
GCD是什么
GCD全称Grand Central Dispatch,是Apple开发的一个异步执行任务的技术之一,GCD将iOS系统中很复杂的多线程开发交给系统来做,开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中就可以实现复杂的多线程开发了。
多线程编程
介绍GCD的使用之前,我们先来复习一下多线程编程中的一些概念。
进程
进程是程序在计算机上一次执行的活动,在iOS中,一般来说,一个App就是一个进程,进程可以向系统要求一块内存空间,一个进程可以包含多个线程。
线程
线程是一个独立执行代码段,一个线程同时间内只能执行一个任务,所以多线程并发就可以在同一时间执行多个任务。
主线程
在iOS中,主线程主要的任务是处理UI事件,所以又叫做UI线程,只有主线程才有负责修改UI的能力,而耗时的操作(加载网络资源、上传文件)一般都放在子线程中,但是开辟子线程会占用一定系统资源,所以一般不要同时开特别多线程。
任务
任务一般指的是需要执行的操作,也就是说线程中执行的那段代码。
同步
同步(sync)一般是指当任务添加到执行任务的队列中时,如果队列中有其他先进入的任务,就会一直等待,直到之前的任务都执行完成后才执行。
同步执行任务只能在当前线程中执行任务,不具备开启新线程的能力。
异步
异步(async)一般是指当任务添加到执行任务的队列中时,不做任何的等待,直接执行任务。
异步执行任务可以在新的线程中执行任务,具备开启新线程的能力。(但是是否开启新线程跟任务所指定的队列有关)
队列
这里的队列指的是执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则,新的任务总是被添加到队尾,读取任务总是从队头读取。
串行
串行队列中每次只会有一个任务被执行。只开启一个线程,一个任务执行完毕后,才执行下一个任务。
并发
并发队列可以让多个任务同时执行。并发队列只有在异步时才会有效。
GCD的使用
苹果的官方说明中说过:
开发者只需要定义想执行的任务并追加到适当的 Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。
也就是说GCD的使用只需要两个步骤:
- 1.找到适当的等待队列。
- 2.将任务追加到等待队列中。
找到适当的等待队列
找打适当的等待队列有两种方法,一种是通过dispatch_queue_create
方法生成Dispatch Queue,还有一种是获取系统提供的Dispatch Queue。
dispatch_queue_create
使用dispatch_queue_create
函数创建队列,需要传入两个参数,第一个作为队列的唯一标示符,可以为空,第二个参数用来识别队列为串行队列还是并发队列。
- DISPATCH_QUEUE_SERIAL 串行队列
- DISPATCH_QUEUE_CONCURRENT 并发队列
|
|
|
|
Main Dispatch Queue (主队列)
实际上我们可以不用可以创建队列,系统也会给我们一共一些队列,Main Dispatch Queue就是其中一个,看名字就能看的出,一个Main出卖了他是在主线程中执行的身份,因为主线程只会有一个,所以Main Dispatch Queue就是串行队列。
获取他可以通过使用dispatch_get_main_queue()
方法获取。
|
|
Global Dispatch Queue(全局并发队列)
有了系统提供的串行队列,我们再看看另一个由系统提供的并发队列 Global Dispatch Queue,全局并发队列,它是所有应用程序都能够使用的并发队列。
获取他可以通过使用dispatch_get_global_queue()
方法获取,需要提供两个参数,第一个参数为优先级,分为:
- DISPATCH_QUEUE_PRIORITY_HIGH 最高优先级
- DISPATCH_QUEUE_PRIORITY_DEFAULT 默认优先级
- DISPATCH_QUEUE_PRIORITY_LOW 低优先级
- DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台优先级
第二个参数暂时没用,传入0
即可。
|
|
将任务追加到等待队列中
GCD提供了同步执行任务的创建方法:
|
|
和异步执行任务的创建方法:
|
|
GCD的使用实例
通过上边的介绍我们大概知道了有两种队列和两种任务的执行方式,排列组合一下就有如下4中结果
- 1.同步执行 + 并发队列
- 2.异步执行 + 并发队列
- 3.同步执行 + 串行队列
- 4.异步执行 + 串行队列
另外我们还介绍了两种系统提供的队列的获取方式,全局并发队列可以作为一种普通的并发队列考虑,但是主队列这里有一些特殊,所以我们单独来讨论他。那么就再加上两种组合。
- 5.同步执行 + 主队列
- 6.异步执行 + 主队列
那么接下来我们就一一来看一下这些组合的实现
同步执行 + 并发队列
我们先来执行如下代码看看结果:
|
|
打印结果如下:
|
|
我们可以看到,所有的任务都是在当前线程中执行的,任务的过程中没有开启新的线程。
另外所有的任务都是在begin和end之间执行的。
尽管并发是不需要等待前一个任务结束就可以开始执行,但是因为同步不具备开启线程的功能,所以也只会有一个线程,所以并发也就不存在了。任务只能一个接一个的执行。
异步执行 + 并发队列
先看示例代码:
|
|
再看打印结果
|
|
可以明显的看到,这次的结果和上次的结果有很大的不同,首先,任务的过程中开启了很多新的线程。
还有,所有的任务都没有在begin和end之间执行,而是在end之后才执行。
说明异步执行是具备开启线程的能力,并且没有等待之前的任务结束,就继续执行任务了。
同步执行 + 串行队列
先看代码:
|
|
再看打印的结果:
|
|
我们可以看到,这次的任务打印结果和第一次异步执行 + 并发队列的结果基本相同,所有的任务都是在主线程执行的,没有开启新的子线程。
并且所有的任务都是在begin和end之间一个一个按顺序执行的。
异步执行 + 串行队列
还是先看代码:
|
|
再看结果
|
|
我们可以看到,所有的任务都是在begin和end之后执行的,并且开启了一条新的线程。
但是因为是串行任务,所以所有的任务还是都是按照顺序一个一个执行的。
同步执行 + 主队列
先看代码:
|
|
再看结果:
|
|
这时候我们会发现,程序走到begin之后就没有再往下进行了,而且还发生了崩溃。
原因就是我们在主线程中执行了syncMain
方法,相当于把syncMain
放到了主线程的队列中,但是同步执行的方式会等到当前队列中的任务执行完毕,才会接着执行,这时候我们又把1号任务追加到主线程中,1号任务就在等待主线程处理完syncMain
的任务,而syncMain
需要等待1号任务执行完毕,才能继续进行。
那这时候就出现了死锁的问题,大家互相等待,所以就都没办法执行下去了。
那如果我们不再主线程中调用呢?
|
|
再看结果:
|
|
这时候我们发现,一切又可以很好的运行起来了,而且都是在主线程中一个一个按照顺序执行的。
那为什么这时候不会发生死锁问题呢?
是因为syncMain
放到了其他的线程中,而1号任务、2号任务和3号任务都被追加到了主线程中,这三个任务都会在主线程中执行,而syncMain
则是在另外一个子线程中执行的,这时候主队列中就没有等待执行的任务,所以就会直接执行1号任务,所以不会发生死锁。
异步执行 + 主线程
接下来我们看一下异步执行 + 主线程的情况:
|
|
再看打印的结果:
|
|
可以看到,所有的任务都会追加到主线程中来执行,但是都会是在end之后,也就是说不会等待之前的任务完成就会继续后边的任务,但是因为所有的任务都被追加到主线程这一条线程中执行,是串行队列,所以任务都是一个接一个的按顺序执行的。
总结
6种组合方法都已经使用过了,接下来让我们来列个表格总结一下吧。
并发队列 | 串行队列 | 主队列 | |
---|---|---|---|
同步 | 不开启新线程,串行执行任务 | 不开启新线程,串行执行任务 | 主线程调用:发生死锁,卡死。其他线程调用:不开启新线程,串行执行任务 |
异步 | 开启新线程,并发执行任务 | 开启一条新线程,串行执行任务 | 不开启新线程,串行执行任务 |
GCD 的其他API
除了刚才介绍过的dispatch_queue_create()
、dispatch_sync()
、dispatch_async()
…之外,GCD的作用还有很多,接下来我们就来看看GCD的其他方法。
dispatch_set_target_queue
这个函数主要有两个作用:
- 1.改变队列的优先级;
- 2.防止多个串行队列并发执行;
改变队列的优先级
使用 dispatch_queue_create函数生成的队列,无论是串行的还是并发的,都是与默认优先级Global Dispatch Queue一致。
如果想要改变队列的优先级,就需要使用到dispatch_set_target_queue。
|
|
第一个参数:需要改变优先级的队列;
第二个参数:指定要执行的目标队列;
举个例子:
生成一个后台的串行队列
|
|
防止多个串行队列并发执行
我们还可以使用dispatch_set_target_queue函数将目标函数指定为某个串行队列(Serial Dispatch Queue),就可以防止这些处理并发执行。
举个例子:
|
|
输出:
|
|
这段代码运行了之后我们会发现,串行队列没有按照顺序一个一个执行。
接下来,我们添加一个目标队列之后
|
|
可以看到任务就按照我们的添加顺序执行了。
|
|
dispatch_after
这个函数可以用来延迟执行block中的代码。
|
|
一般情况下,我们只需要修改delayInSeconds
就可以了。
举个例子:
比如我想要3秒之后执行某个事情
|
|
但是这里需要注意一点,这里的时间不是绝对的3秒后,而是在3秒后追加到Dispatch Queue中,效果和在3秒后使用dispatch_async追加到Main Queue效果相同。
因为Main Dispatch Queue是在主线程的RunLoop中执行的,所以在比如每个1/60秒执行的RunLoop中,block最快在3秒后执行,最慢在3+1/60秒后执行。但是如果主线程本身有延迟的话,这个时间就会更长一些了。
dispatch_once
这个函数可以用来保证在应用程序执行中只执行一次操作。一般在创建单例的时候经常使用,即使在多线程的情况下,也能保证线程的安全。
|
|
dispatch_group
这个函数一般用来实现这样的需求,有三个耗时任务A、B、C,其中C需要等A和B异步执行完之后才能执行。这个时候我们就可以用到队列组来实现。
队列组中有三种方法能够实现这样的需求。
- 1.使用 dispatch_group_notify
- 2.使用 dispatch_group_wait
- 3.使用 dispatch_group_enter,dispatch_group_leave
dispatch_group_notify
这个方法可以监听group中任务的完成状态,当所有的任务都执行完成之后,追加任务到group中,并执行任务。
|
|
打印结果:
|
|
可以看到,任务A和任务B是异步的,没有按照顺序执行,但是任务C是在A和B都结束之后才开始执行的。
dispatch_group_wait
这个方法可以阻塞当前线程,等待group中的任务全部完成之后,才会继续往下执行。
这里边的第二个参数用来表示阻塞当前线程的时间,如果传入DISPATCH_TIME_FOREVER
则表示永远,直到group中的任务执行完毕。
|
|
打印结果:
|
|
dispatch_group_enter + dispatch_group_leave
dispatch_group_enter 表示将一个任务追加到了group中,执行一次,则表示group中未完成的任务+1;
dispatch_group_leave 表示一个任务离开了group中,执行一次,则表示group中未完成的任务-1;
当group中未完成的任务数为0的时候,才会开始执行下边的任务。
|
|
输出结果:
|
|
dispatch_barrier_async
这个方法有一个很接地气的方法,叫做栅栏方法,听名字大概就能明白意思了,这个方法就是为了把队列中的任务分割开的。
当我们在异步执行的时候,有两组操作,并且只有当第一组操作全部执行完之后再执行第二组操作时就可以用到这个方法。
|
|
输出结果
|
|
dispatch_apply
通过这个函数,我们可以按照指定次数,将block追加到制定队列中,并等待任务全部执行完之后执行后边的代码。
|
|
输出:
|
|
这个函数可以使用并发队列异步执行所有的任务,所以也可以用来遍历数组中的内容,只是顺序并不会按照数组的下标顺序来
|
|
输出
|
|
dispatch_semaphore
dispatch semaphore 称为GCD中的信号量,是持有计数的信号,当计数为0的时候等待,不可通过,当计数大于等于1时,计数减一且不等待,可通过。
dispatch_semaphore提供了三个函数:
- dispatch_semaphore_create 创建一个Semphore并初始化信号的总量。
- dispatch_semaphore_signal 发送一个信号,让信号总量加1。
- dispatch_semaphore_wait 可以使总信号量减1,让信号总量为0时就会一直等待,否则就可以正常执行。
dispatch semaphore在开发中主要用于
- 保持线程同步,将异步执行任务转换为同步执行任务
- 保证线程安全,为线程加锁
保持线程同步
使用dispatch semphore,可以使异步执行的任务转换成同步执行的任务,比如下边代码中,我们添加了一个异步执行的方法:
|
|
输出结果:
|
|
但是看到结果,end方法是在最后才执行的,semaphore将异步任务转换成了同步任务。
保证线程安全
如果我们的应用中,有一个变量,每个线程都可以对他进行读写,那如果多个线程对他同时进行读写,就会影响到线程安全。
举个例子,有两个线程,线程A和线程B,他们同时都要对某个变量进行+1操作,如果此时变量值为10,线程A执行读取操作时,线程B也执行了读取操作,他们又都进行了+1操作,这时候实际上是执行了两次+1,但是,他们将变量写回去时却只加了一次,这就影响了线程安全。
下边用一个火车票售卖的方式来看看线程安全的问题。
比如,一共有50张火车票,共有AB两个售票口,他们同时卖票,直到卖完为止。
非线程安全时:
|
|
可以看到打印结果:
|
|
有很多时候票数是乱的,这样的结果就是线程不安全的情况。
线程安全时:
|
|
输出结果:
|
|
这时候就可以看到,票的数量都是按照正常的顺序一张一张减少,也没有出现错乱的问题,这时候就是线程安全的了。
最后
以上差不多就是GCD的全部内容了,这里也只是多线程开发的一小部分,写这一部分的大神也有很多,这篇文章很多内容都是参考自他们的博客,还有一部分来自于《Objective-C 高级编程》的第三章,本文章仅限个人学习使用,如果有什么不对的地方还请大佬们多多指教。
啊,对了,最后还是放一个demo地址吧。