前言
终于来到了多线程这一篇章,多线程我个人觉得是整个OC里面比较重要的一环,因为不管是从底层(lisdispatch)还是到应用层,都是使用非常广泛的。本文主要是对整个多线程原理作一个简单地梳理,然后结合最常使用的GCD作进一步的解释。
什么是线程?什么是进程?
-
线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。
-
进程要想执行任务,必须得有线程,进程至少要有一条线程
-
程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程
-
进程是指在系统中正在运行的一个应用程序
-
每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内
-
进程有内存空间,线程没有空间 -
同一进程内的线程共享本进程的资源,但是进程之间的资源是独立的。
-
进程的崩溃不会影响其他进程,但是线程的崩溃会直接导致所在的进程崩溃
-
进程之前的切换需要花费更多的资源,而线程会更优。
-
线程是处理器调度的基本单位,但是进程不是。
由于IOS是单进程,如果只是单线程,会导致任务进度缓慢,所以我们需要多线程。
意义
优点
-
能适当提高程序的执行效率
-
能适当提高资源的利用率(CPU,内存)
-
线程上的任务执行完成后,线程会自动销毁
缺点 -
开启线程需要占用一定的内存空间(默认情况下,每一个线程都占 512 KB)
-
如果开启大量的线程,会占用大量的内存空间,降低程序的性能
多线程原理
CPU具有调度能力,在让一个线程去处理一个任务的时候,他不会等待他执行完成,而是会切换到其他线程,调度其他线程执行其他任务,因为执行速度很快,所以根本感知不到,同一时间只有一个线程在执行任务,如果要实现真正意义上的并发,那就是多核CPU。
线程的状态
线程状态:当一个线程被创建的时候,并不会立即执行,而是进入runable准备阶段,等待cpu的调度,cpu一旦调度就会进入到runing,running之后要么死亡,要么会阻塞,之后会重新进入到runable等待调度。
可调度线程池:线程的创建不是顺序的。当新来一个任务的时候,首先会判断当前可调度线程池中是否存在空闲的线程,如果存在,安排线程去执行,如果不存在,再判断是否达到了阈值,如果没有,那么就会把这条任务放到队列中,再安排线程执行。如果都已经饱满并且都处于执行状态,就会交给饱和策略。
饱和策略:1.抛出异常。 2.任务回退给调用者。 3.丢掉等待过久的任务 4.丢弃任务。
任务执行
任务的执行速度的影响因素
1.cpu 2.任务的复杂度 3.优先级 4.线程状态
优先级翻转
IO密集型:频繁等待的线程。
CPU密集型:很少等待的线程。
对于频繁等待的线程,因cpu具有调度能力,会对该线程提升优先级,但提升了并不是意味着会立即执行,仅仅是提高优先级。
优先级因素:1.用户指定 2.频繁等待程度 3.长时间不执行
锁
在多线程中,因为在同一个进程中,资源是共享的,所以多线程必然存在着资源的竞争,那么就引入了锁的概念。
自旋锁:发现线程正在执行,不停地询问,资源消耗比较高,典型的就是atomic。适用于短小精简的任务。
互斥锁:发现线程正在执行,进入休眠,等待唤醒,资源消耗低。
atomic
ios属性的默认值,原子属性,单写多读,需要消耗大量资源。
GCD简介
一套c语言的api。
GCD 是苹果公司为多核的并行运算提出的解决方案
GCD 会自动利用更多的CPU内核(比如双核、四核)
GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码
串行队列与并发队列
串行队列:任务之间是顺序的,挨个排好队。
并发队列:任务之间调度是并发的,任务之间的调度是不确定的,1234任务都有可能,如果是比较长的任务一时完不成可能会挂起,其他任务完成快的会先执行,先调度并不定会先执行。任务的执行依赖于线程。
死锁简单案例
dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
分析: 首先这是一个串行队列,串行队列执行是顺序的,走到1,发现异步函数,可以缓一缓,接下来去5,然后进入到异步函数,走到2也没问题,接来来是一个同步函数,同步函数的执行堵塞了4的执行,4的执行需要等3执行完才会执行,但是3的执行是后来添加的,并且是dispatch_sync同步的,需要等4执行完才可以执行,互相等待对方执行造型死锁。
dispatch_queue_t queue = dispatch_queue_create("cooci", **NULL**);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
});
NSLog(@"5");
分析:前面和上个案例一样,在执行3的时候,由于是同步函数,需要等待最外层的异步函数执行完毕才会执行,但是它本身就在这个异步函数内部,所以走不完,造成死锁。
WB面试题
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
dispatch_sync(queue, ^{ NSLog(@"3"); });
NSLog(@"0");
dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});
1230789
分析:这是一个串行队列,串行队列中,执行任务是顺序执行。即使是异步函数,开辟了新的线程,任务也是要等待执行完毕以后才会继续执行下面的任务。可以在23任意位置加入sleep看一下线程的堵塞情况,如果是在2处,他会在1打印完成以后,sleep结束以后才会打印2,也就是说串行队列会影响到执行.
MT面试题
while (self.num < 5) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.num++;
});
}
NSLog(@"end : %d",self.num);
问题的点:异步函数并发,num的值在多个线程被赋值。while循环,小于5出不来,但是这是一个全局并发队列。所以num>=5.又做了个实验,把他改成了串行队列,一样也是大于5的,不同的是,串行队列相比较并行队列,更接近于5.
KS面试题
for (int i= 0; i<10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.num++;
});
}
分析:for循环跟while不同,for循环是确定了循环次数,只循环一万次,一万次完成以后,不管你值有没有改变都打印了,所以numer的值小于等于10000.