多线程的问题一般面试都会问到,基础知识一般问的不多,但是还要过一下,因为有些面试官的水平不一定很高。他们大多问的是GCD,因为大家都一样,都是用这个,有的会问GCD的函数调用,相互直接的区别,会出一些场景题给你,延深一点会问到锁。
一、进程和线程
1.什么是进程
进程是指在系统中正在运行的一个应用程序每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
比如同时打开QQ、Xcode,系统就会分别启动2个进程通过“活动监视器”可以查看Mac系统中所开启的进程
2.什么是线程
1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程)线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行比如使用酷狗播放音乐、使用迅雷下载电影,都需要在线程中执行
二.多线程的优缺点
多线程的优点:
能适当提高程序的执行效率
能适当提高资源利用率(CPU、内存利用率)
多线程的缺点:
开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能线程越多,CPU在调度线程上的开销就越大程序设计更加复杂:比如线程之间的通信、多线程的数据共享
三.多线程的方式
NSThread 我们经常在调试的时候会用它查看当前的线程[NSThread currentThread]
NSOperation ----> iOS 2.0(后来苹果改造了NSOperation的底层)
-将操作(异步执行)添加到队列(并发/全局)
- [NSOperationQueue mainQueue]主队列。 任务添加到主队列, 就会在主线程执行
-提供了一些GCD不好实现的,”最大并发数“
-暂停/继续 --- 挂起
-取消所有的任务
-依赖关系
四:GCD 任务和队列
学习 GCD 之前,先来了解 GCD 中两个核心概念:任务和队列。
任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行(sync)和异步执行(async)。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
同步执行(sync):
同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async):
异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
可以在新的线程中执行任务,具备开启新线程的能力。
队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。队列的结构可参考下图:
在 GCD 中有两种队列:串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。
串行队列(Serial Dispatch Queue):
每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Dispatch Queue):
可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
注意:并发队列的并发功能只有在异步(dispatch_async)函数下才有效
两者具体区别如下两图所示。
五. 队列的创建方法/获取方法
可以使用dispatch_queue_create来创建队列,需要传入两个参数,第一个参数表示队列的唯一标识符,用于 DEBUG,可为空,Dispatch Queue 的名称推荐使用应用程序 ID 这种逆序全程域名;第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并发队列。
// 串行队列的创建方法dispatch_queue_tqueue= dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法dispatch_queue_tqueue= dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
对于串行队列,GCD 提供了的一种特殊的串行队列:主队列(Main Dispatch Queue)。
所有放在主队列中的任务,都会放到主线程中执行。
可使用dispatch_get_main_queue()获得主队列。
// 主队列的获取方法dispatch_queue_tqueue= dispatch_get_main_queue();
对于并发队列,GCD 默认提供了全局并发队列(Global Dispatch Queue)。
可以使用dispatch_get_global_queue来获取。需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用0即可。
// 全局并发队列的获取方法dispatch_queue_tqueue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
六. 任务的创建方法
GCD 提供了同步执行任务的创建方法dispatch_sync和异步执行任务创建方法dispatch_async。
// 同步执行任务创建方法dispatch_sync(queue, ^{// 这里放同步执行任务代码});
// 异步执行任务创建方法dispatch_async(queue, ^{// 这里放异步执行任务代码});
七. GCD 的使用步骤
GCD 的使用步骤其实很简单,只有两步。
创建一个队列(串行队列或并发队列)
将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)
以上就是GCD的一些基础知识,基本上都要很熟练的掌握。下面说一下GCD的用法,这个很重要。
八.GCD的用法
1.GCD 栅栏方法:dispatch_barrier_async (这个很多人会问)
我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。
dispatch_barrier_async函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在dispatch_barrier_async函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。具体如下图所示:
在dispatch_barrier_async执行结果中可以看出:
在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作。
2.GCD 延时执行方法:dispatch_after
可以看到延时函数是在主线程中执行的。
3. GCD 一次性代码(只执行一次):dispatch_once
我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once 函数=
4.GCD 快速迭代方法:dispatch_apply
通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的函数dispatch_apply。dispatch_apply按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
如果是在串行队列中使用dispatch_apply,那么就和 for 循环一样,按顺序同步执行。可这样就体现不出快速迭代的意义了。
我们可以利用并发队列进行异步执行。比如说遍历 0~5 这6个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply可以 在多个线程中同时(异步)遍历多个数字。
还有一点,无论是在串行队列,还是异步队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的dispatch_group_wait方法。
因为是在并发队列中异步执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。但是apply---end一定在最后执行。这是因为dispatch_apply函数会等待全部任务执行完毕。
5.GCD 队列组:dispatch_group 和 dispatch_group_notify/dispatch_group_wait
有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。
调用队列组的dispatch_group_async先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的dispatch_group_enter、dispatch_group_leave组合 来实现
dispatch_group_async。
调用队列组的dispatch_group_notify回到指定线程执行任务。或者使用dispatch_group_wait回到当前线程继续向下执行(会阻塞当前线程)。
6.dispatch_group_enter、dispatch_group_leave
dispatch_group_enter标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1
dispatch_group_leave标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1。
当 group 中未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞,以及执行追加到dispatch_group_notify中的任务。
从dispatch_group_enter、dispatch_group_leave相关代码运行结果中可以看出:当所有任务执行完成之后,才执行 dispatch_group_notify 中的任务。这里的dispatch_group_enter、dispatch_group_leave组合,其实等同于dispatch_group_async
7.GCD 信号量:dispatch_semaphore
dispatch_semaphore_t dispatch_semaphore_create(long value):创建信号量,参数:信号量的初值,如果小于0则会返回NULL
long dispatch_semaphore_signal(dispatch_semaphore_t dsema):发送一个信号,让信号总量加1
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
等待降低信号量,接收一个信号和时间值(多为DISPATCH_TIME_FOREVER)
若信号的信号量为0,则会阻塞当前线程,直到信号量大于0或者经过输入的时间值;
若信号量大于0,则会使信号量减1并返回,程序继续住下执行
应用场景
保持线程同步,将异步操作转换为同步操作
如果注掉dispatch_semaphore_wait这一行,则 j = 0;
注释: block块异步执行添加到了全局并发队列里,所以程序在主线程会跳过block块(同时开辟子线程异步执行block块),执行块外的代码dispatch_semaphore_wait,因为semaphore信号量为0,且时间为DISPATCH_TIME_FOREVER,所以会阻塞当前线程(主线程),进而只执行子线程的block块,直到执行块内部的dispatch_semaphore_signal使得信号量+1。正在被阻塞的线程(主线程)会恢复继续执行。这样保证了线程之间的同步。
为线程加锁
知识点就是上面这么多,我想足以应对面试问题了,下面是我自己遇到的一个问题。
同步任务情况下,串行队列和主队列有啥区别,为啥在主队列会死锁,串行队列就不死锁?
首先执行任务1,这是肯定没问题的,只是接下来,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3。但这是队列,有任务来,当然会将任务加到队尾,然后遵循FIFO原则执行任务。那么,现在任务2就会被加到最后,任务3排在了任务2前面,任务3要等任务2执行完才能执行,任务2又排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等待的局面,这就是死锁现象。
两个图中有很明显的区别,都是在主线程中,但是一个是主队列,一个是并发队列,其实跟串行队列的作用是一样的,不同点是第一个图画了一条执行路径,死锁是自己给自己添堵,第二幅图是两条执行路径,并发队列的任务执行完毕后会返回主队列,相当于是在主线程强行插入一个任务,所以没有卡。