多线程优化
1 进程,线程介绍
进程:
进程是指系统中正在运行的一个应用程序。每个进程之间是独立的。每个进程均运行在其专有且受保护的内存空间内。进程占独立的内存空间。
线程
线程是进程的基本执行单元。一个进程想要执行任务,必须得有线程。进程里面的所有任务都在线程中执行。每一个进程打开程序的时候都会默认开启一条线程。
我们称之为主线程。一个线程包括:独有ID ,程序计数器 (Program Counter),寄存器集合,堆栈。 进程可以有很多线程。他们共享进程的全局变量,堆数据以及内存空间。
我们知道 线程和进程都是虚拟的概念。实际上都是 CPU 在执行任务。程序计数器简称PC 指向的是当前的指令地址。程序通过更新这个PC来让CPU执行任务。
实际上 PC 是 CPU 核心中的寄存器,它是实际存在的,所以也可以说一个 CPU 核心同一时刻只能执行一个线程。同一时间,单核cpu只能处理1条线程,只有1条线程在工作(执行)
当前设备都是多核系统。意义就是具有多个CPU核心。而一个CPU核心同一时间只能执行一个PC,也就是只能执行一个线程。当我们线程不超过多核设备的核心CPU数量之时
确实可以做到多条线程真正意义上同时执行。但是当线程数量超过CPU核心数量的时候,一个核心CPU就需要处理多条线程。这个行为叫做线程调度。此时,CPU会在多条线程之间调度(切换)
执行任务。当CPU切换速度足够快的时候,就造成了假象的同时运行,这就称之为多线程并发(当然实际里面操作会更复杂)。
2 iOS 多线程 NSThread NSOperation CGD
以上这三种编程方式从上到下,抽象度层次是从低到高的,抽象度越高的使用越简单,也是Apple最推荐使用的。
- NSThread
优点:NSThread 比其他两个轻量级,使用简单
缺点:需要自己管理线程的生命周期、线程同步。线程同步对数据的加锁会有一定的系统开销 - NSOperation
不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上 -
GCD
Grand Central Dispatch是由苹果开发的一个多核编程的解决方案。是替代NSThread, NSOperation的高效和强大的技术,用途广泛,苹果公司极力推崇的一个框架。
- NSThread
- init方式
- detachNewThreadSelector创建好之后自动启动
- performSelectorInBackground创建好之后也是直接启动
- (void) createNSThread{
NSString *threadName1 = @"NSThread1";
NSString *threadName2 = @"NSThread2";
NSString *threadName3 = @"NSThread3";
NSString *threadNameMain = @"NSThreadMain";
NSThread *thread1 = [[NSThread alloc] initWithTarget:self
selector:@selector(doSomething:) object:threadName1];
[thread1 start];
[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self
withObject:threadName2];
[self performSelectorInBackground:@selector(doSomething:)
withObject:threadName3];
//运行在主线程,waitUntilDone:是否阻塞等待@selector(doSomething:)执行完毕
[self performSelectorOnMainThread:@selector(doSomething:)
withObject:threadNameMain waitUntilDone:YES];
}
- (void) doSomething:(NSObject *)object{
NSLog(@"%@:%@", object,[NSThread currentThread]);
}
- NSThread的常用类方法
//退出线程
[NSThread exit];
//判断当前线程是否为主线程
[NSThread isMainThread];
//判断当前线程是否是多线程
[NSThread isMultiThreaded];
主线程的对象
NSThread *mainThread = [NSThread mainThread];
//线程是否在执行
thread.isExecuting;
//是否被取消
thread.isCancelled;
//是否完成
thread.isFinished;
//是否是主线程
thread.isMainThread;
//线程的优先级,取值范围0.0-1.0,默认优先级0.5,1.0表示最高优先级,优先级高,CPU调度的频率高
thread.threadPriority;
- GCD
任务
任务 :就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。
执行任务有两种方式:同步执行(sync)和异步执行(async)。
两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
队列:
队列(queue):这里的队列执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出) 的原则,即 新任务总是被插入到队列的末尾,而读取任务的时候总是 从队列的头部开始读取 。每读取一个任务,则从队列中释放一个任务。
队列有两种:
- 串行 :每次只有一个任务被执行,让任务一个接着一个地执行(只开启一个线程)。
- 并行 :可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)。
GCD总结:将任务(要在线程中执行的操作block)添加到队列(自己创建或使用全局并发队列),并且制定执行任务的方式(异步或同步)。
总结 : GCD 主要 先看 任务 是同步 还是异步。 只要是 同步的 就代表 并不会开辟线程。 所有代码 只在当前线程里面执行 (就是 dispatch_sync()方法调用的线程执行),如果是异步的 代表 具备开辟线程能力了。 并且代码 是异步执行的。并不会堵塞当前线程代码。 然后 看队列。 串行 队列 代表着 这个队列里的代码 都是 同步执行的。按顺序执行的,如果是同步添加的 同步添加并不会开辟线程。如果是异步添加的 ,串行队列 只会开辟一个线程。 所有添加进来的代码 都只是串行执行。并行队列: 如果是 同步的 也不会开辟线程。乖乖的变成了同步执行。 如果是异步执行 就可以开辟多条线程
简单的代码事例
- (void)syncSerial {
NSLog(@"currentThread: %@", [NSThread currentThread]);
NSLog(@"syncSerial begin");
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];// 模拟耗时操作
NSLog(@"task1--%@", [NSThread currentThread]);// 打印当前线程
}
});
dispatch_sync(queue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];// 模拟耗时操作
NSLog(@"task2--%@", [NSThread currentThread]);// 打印当前线程
}
});
dispatch_sync(queue, ^{
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2];// 模拟耗时操作
NSLog(@"task3--%@", [NSThread currentThread]);// 打印当前线程
}
});
NSLog(@"syncSerial end");
}
GCD : 群组group:
1 . 先 创建 dispatch_group_t group = dispatch_group_create(); 调度群组
- 创建 dispatch_queue_t queue= dispatch_queue_create("groupQueue", DISPATCH_QUEUE_CONCURRENT); 群组队列。 (队列 可以是串行 也可以是并行队列。跟正常GCD一样理解)
- 调用 dispatch_group_async 开始加入任务。 (群组只有异步执行。添加队列之后 线程创建跟正常GCD一样理解,)
- 调用 dispatch_group_notify 监听 任务执行完毕。
//创建群组队列
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue= dispatch_queue_create("groupQueue", DISPATCH_QUEUE_CONCURRENT);
//开一条线程 做事情
dispatch_group_async(group, queue, ^{
for (NSInteger i = 0; i<5; i++) {
NSLog(@"s1 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
[NSThread sleepForTimeInterval:3.0f];
NSLog(@"s2 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
}
});
// 开多个线程做事情
// for (NSInteger i = 0; i<5; i++) {
// dispatch_group_async(group, queue, ^{
// NSLog(@"s1 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
// [NSThread sleepForTimeInterval:3.0f];
// NSLog(@"s2 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
//
// });
// }
dispatch_group_notify(group, queue, ^{
NSLog(@"执行后续任务");
});
********** 这里的dispatch_group_async 是放在for in 里面 或者是在dispatch_group_async的BLOCK里面执行循环都可以。 按照正常GCD理解就行。 dispatch_group_async的block 里面执行for in 就是 跟GCD一样解释 异步执行 并行队列。 会开线程。 因为只是执行一次 就开了一个线程。 如果只是考虑只需要异步操作 这样即可。 但是 如果是 for in 里面 添加 dispatch_group_async 那么 理解成为 异步 并行队列 由于 循环添加几次。 就会开辟线程。 至于最后开辟几条线程 由系统控制。*************
dispatch_group_enter 与 dispatch_group_leave 的使用
如果在调度群组关联的block内直接异步提交新的任务,group 不会等待嵌套的异步任务执行完毕后在进入 dispatch_group_notify 和 dispatch_group_wait 状态 (说人话就是 我们在group群组里面再开线程 并不会等我们这个线程执行完毕 在执行dispatch_group_notify)
- (void)groupEnterAndLeave {
//创建群组队列
//如果在调度群组关联的block内直接异步提交新的任务,group 不会等待嵌套的异步任务执行完毕后在进入 dispatch_group_notify 和 dispatch_group_wait 状态
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue= dispatch_queue_create("groupQueue", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i<5; i++) {
dispatch_group_async(group, queue, ^{
NSLog(@"s1 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
[NSThread sleepForTimeInterval:3.0f];
NSLog(@"s2 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2.0f];
NSLog(@"s3 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
});
});
}
dispatch_group_notify(group, queue, ^{
NSLog(@"执行后续任务");
});
}
既上述代码 巡行效果 可以得出 我们在 dispatch_group_async 任务里面 如果另外开辟了线程 做了异步操作 那么 dispatch_group_notify 并不会等我们 这个异步操作执行完毕 再执行 而知忽略, 直接执行dispatch_group_notify。 如何解决这个问题。 就用到了 dispatch_group_enter 与 dispatch_group_leave 来控制。
- (void)groupEnterAndLeave {
//创建群组队列
//如果在调度群组关联的block内直接异步提交新的任务,group 不会等待嵌套的异步任务执行完毕后在进入 dispatch_group_notify 和 dispatch_group_wait 状态
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue= dispatch_queue_create("groupQueue", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i<5; i++) {
dispatch_group_async(group, queue, ^{
NSLog(@"s1 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
[NSThread sleepForTimeInterval:3.0f];
NSLog(@"s2 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
//计数 +1
dispatch_group_enter(group);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2.0f];
NSLog(@"s3 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
//计数-1
dispatch_group_leave(group);
});
});
}
dispatch_group_notify(group, queue, ^{
NSLog(@"执行后续任务");
});
}
等待群组任务完成disaptch_group_wait
disaptch_group_wait :
同步等待事先提交到群组中的任务完成。在指定的超时期限过去之前,返回这些block是否完成。当发生超时时,这个群组将恢复到原来的状态。
如果这个调度群组是空的(没有block与这个群组相关联),这个函数立即返回。
在这个函数成功返回之后,这个调度群组是空的。它既可以使用dispatch_release释放掉,也可以重新添加block。
成功(在指定的超时期限内,所有与群组相关联的block完成)将返回零。失败(超时发生)返回非零。
group,等待完成的调度群组。不可以为NULL。
timeout,超时时间(参考dispatch_time)。常量DISPATCH_TIME_NOW和DISPATCH_TIME_FOREVER被提供使用很方便。 一般我们使用DISPATCH_TIME_FOREVER 一直等下去。超时时间不限。
disaptch_group_wait 会卡住当前线程。 叫后面的代码不执行 知道 之前添加到群组里面的任务 完成 再执行后面代码。
GCD 信号量
理解信号量我们必须了解一下三个函数:
- dispatch_semaphore_create(long value);创建信号量,参数为设置信号量的初始值。
- dispatch_semaphore_signal(dispatch_semaphore_t dsema);发送当前信号量,参数为当前创建的信号量。
- dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);等待信号量,第一个为当前等待的信号量,第二个参数为超时时间。当等待时间超过超时时间就不会继续等待了。
信号量 可以理解为 创建信号的时候 填写一个value 默认值为0 当我们调用 dispatch_semaphore_wait 的时候 会使这个value 值-1 . 而在我们 调用dispatch_semaphore_signal 的时候 会使 这个value +1 我们在写dispatch_semaphore_wait 代码的地方 作为一个节点。 当这个value值 大于等于 0 的时候 才可以 继续执行dispatch_semaphore_wait 后面的代码。 并且 dispatch_semaphore_wait的代码 算是一个监控。 随时等到 dispatch_semaphore_wait 调用 使这个值+1 到达 0的时候 dispatch_semaphore_wait 后面的代码 才可以执行。
代码实例 我们在 dispatch_semaphore_wait 的地方 此时 value 为0 dispatch_semaphore_wait会使 value -1 所以此时 value 值为 - 1 所以 dispatch_semaphore_wait后面代码不能执行。需要等待 dispatch_semaphore_signal 虽然 是 dispatch_async异步执行的并行队列 按理开线程的。 但是这里就堵塞了。(从代码运行效果来看 既然是前一个执行完毕 后一个才执行 所以线程都是一个)
- (void)xinhaoliang {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_queue_t queue1 = dispatch_queue_create("queueSemaphore1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2= dispatch_queue_create("queueSemaphore2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue1, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"并行队列信号1:%d", i);
NSLog(@"s1 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
}
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue2, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"并行队列信号2:%d", i);
NSLog(@"s2 线程:%@ -- index = %ld",[NSThread currentThread],(long)i);
}
});
}
归总:GCD 信号量 是可以做的事
- 1 . 可以将 异步执行的并行线程(不管是同队列 还是不同队列的)queue 都可以做到串行 执行。(参考上图)可以将AFN请求的时候 不同接口 搞成同步请求😁。
- 可以做到控制最大并发量。 (变为控制线程数量)GCD 不能控制线程开辟个数。 但是通过变相控制最大并发量 达到控制控制线程个数。(只是变相控制。 因为并不能完全控制线程数量 只是大大使线程重用);
- (void)xinhaoNumber {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
for (int i = 0; i < 11; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"并发线程数量:%d 当前线程:%@",i,[NSThread currentThread]);
sleep(5);
dispatch_semaphore_signal(semaphore);
});
}
}
NSOperation
- NSOperation简介:NSOperation是基于GCD之上的更高一层封装,NSOperation需要配合NSOperationQueue来实现多线程。
- NSOperatino实现多线程的步骤如下:
··· - 1.创建任务:先将需要执行的操作封装到NSOperation对象中。
- 2.创建队列:创建NSOperationQueue。
- 3.将任务加入到队列中:将NSOperation对象添加到NSOperationQueue中。
···
需要注意的是,NSOperation是个抽象类,实际运用时中需要使用它的子类,有三种方式:
··· - 1.NSInvocationOperation
- 2.NSBlockOperation
- 3.运用继承自NSOperation的子类
···
- (void)nsoperationQueue {
NSLog(@"nsoperationQueue start");
//创建队列
NSOperationQueue * queue = [[NSOperationQueue alloc]init];
//运用最大并发数实现串行 运用队列的属性maxConcurrentOperationCount(最大并发数)来实现串行,值需要把它设置为1就可以了,下面我们通过代码验证一下。
queue.maxConcurrentOperationCount = 2;
//创建任务 操作类 NSOperation 子类化 //创建NSInvocationOperation
NSInvocationOperation * invocationOperation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(operationAddOperation) object:nil];
//创建NSBlockOperation
NSBlockOperation * blockOperation = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 3; i++) {
NSLog(@"NSBlockOperation: %@", [NSThread currentThread]);
}
}];
NSBlockOperation * blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 3; i++) {
NSLog(@"NSBlockOperation22222====: %@", [NSThread currentThread]);
}
}];
[queue addOperation:invocationOperation];
[queue addOperation:blockOperation];
[queue addOperation:blockOperation2];
NSLog(@"nsoperationQueue end");
}
GCD 死锁理解
想要了解死锁,必须充分理解以下两个概念!
- 同步 sync
- 异步 async
- 串行队列
- 并行队列
同步 sync: 概念是指: block 里的任务 需要同步执行, 执行完毕 block 里面的任务 才能执行block后面的代码。(这里的block后边代码是指跟sync 在同一队列的代码)
异步 async:概念是指: block 里面的任务 跟后边的代码可以同时执行, 不需要等待。
串行队列 :概念是指:任务是依次执行的 ,先加到队列的 先执行, 后加到队列的后执行, (什么时间加到队列 看什么时候sync进来的任务)
并行队列:概念是指:添加到队列的任务 不分先后, 加进来 就可以异步执行。
使用sync函数 往当前串行队列里面添加任务 会造成死锁现象! 解释当前串行队列: 就是当前的线程开辟的时候使用的串行队列生成的线程。然后 再在线程里面利用sync函数用生成本身运行线程的串行队列里面添加任务。就会形成死锁。
- 形成死锁现象 1. 使用sync函数 2.往当前串行队列添加任务 造成 当前任务等带薪任务 薪任务等待当前任务完成再执行, 导致死锁。
GCD 如何模拟 NSOPeration 的依赖。
- 异步串行队列任务 可以保证 俩个任务 顺序执行
- 利用多组 group ,每个群组的enter 和leave ,之后 会执行 群组完成的任务, 这个任务 就是依赖别的任务的任务。
3.信号量值为1 , 在要依赖的blcok 里面 信号量 wait , 在完成依赖的任务里面 signal