目录
一、基本概念
- 1.多线程
- 2.串行和并行, 并发
- 3.队列与任务
- 4.同步与异步
- 5.线程状态
- 6.多线程方案
二、GCD
- 1.GCD简介
- 2.GCD的优势
- 3.GCD任务和队列
- 4.任务的管理(调度)
- 5.任务的执行
- 6.同步,异步,并发,串行的组合
- 7.GCD其他函数
- 7.1延迟执行
- 7.2 一次性代码
- 7.3 队列组
- 7.4 快速迭代dispatch_apply函数
- 7.5.dispatch_barrier_async
- 7.6.补充
三、NSOperation和NSOperationQueue
- 1.NSOperation
- 1.1NSInvocationOperation
- 1.2.NSBlockOperation
- 1.3.自定义NSOperation
- 1.4.操作之间的关系 -- 操作依赖
- 1.5.操作的监听(KVO)
- 2.NSOperationQueue
- 2.1使用
- 2.2设置最大并发数
- 2.3队列的取消,暂停和恢复
四、补充
- 1.GCD和NSOperation区别
- 2.线程间的通信
- 3.线程安全
- 4.熟悉SDWebImage的实现原理
一、基本概念
1.多线程
-
什么是多线程?
进程是资源分配的最小单位,线程是CPU调度的最小单位。
多线程:是指从软件或者硬件上实现多个线程的并发技术。试想一下,一个任务由十个子任务组成。现在有两种方式完成这个任务 1.建十个线程,把每个子任务放在对应的线程中执行。 2.把十个任务放在一个线程里,按顺序执行。 效率后者完胜前者 ?
了解了多线程的原理后,你会有更深的理解
-
多线程的原理
-
1个线程中任务的执行是串行的
- 如果要在1个线程中执行多个任务,那么只能一个一个地按顺序执行这些任,也就是说,在同一时间内,1个线程只能执行1个任务
-
1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务
对于单核
- 同一时间,单核CPU只能处理1条线程,只有1条线程在工作(执行)
- 多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
对于多核
- 多线程并发(同时)执行,就是多条线程同时执行任务
并不是所有的框架都支持多线程, 必须要有多核的cpu支持才行, 单核cpu即使开了多线程运行速度也不会有变化,过多的线程会占用大量的内存,目前主线程和子线程的开销均为512kb
-
多线程的好处:
(1)使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片、视屏的下载
(2)发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好(多核)多线程的缺点:
(1)大量的线程降低代码的可读性;
(2)更多的线程需要更多的内存空间
(3)当多个线程对同一个资源出现争夺时候要注意线程安全的问题
2.串行和并行, 并发
并行指的是一种技术,一个同时处理多个任务的技术。它描述了一种能够同时处理多个任务的能力,侧重点在于“运行”。
并行的反义词就是串行,表示任务必须按顺序来,一个一个执行,前一个执行完了才能执行后一个。
并发指的是一种现象,一种经常出现,无可避免的现象。它描述的是“多个任务同时发生,需要被处理”这一现象。它的侧重点在于“发生”。
比如在一个景点,一个游客买票对应售票处来说,当做一个任务,而很多游客需要买票,同时发生,需要被处理,这种现象称为并发
景点开放了多个买票窗口,同一时间内能服务多个游客。这种情况可以理解为并行
-
进入景点的控制门,一次只能一个人通过,这叫串行;
我们经常挂在嘴边的“多线程”,正是采用了并行技术,从而提高了执行效率。因为有多个线程,所以计算机的多个CPU可以同时工作,同时处理不同线程内的指令。
3.队列与任务
队列只是负责任务的调度,而不负责任务的执行, 任务是在线程中执行的
队列的特点:先进先出,排在前面的任务最先调度
串行队列:任务按照顺序被调度,前一个任务不执行完毕,队列不会调度
并行队列:只要有空闲的线程,队列就会调度当前任务,交给线程去执行,不需要考虑前面是都有任务在执行,只要有线程可以利用,队列就会调度任务。
不管是串行队列(SerialQueue)还是并行队列(ConcurrencyQueue),都是FIFO队列。也就意味着,任务一定是一个一个地,按照先进先出的顺序来调度。
4.同步与异步
同步
(sync)
和 异步(async)
的主要区别在于会不会阻塞当前线程,直到 Block 中的任务执行完毕!
如果是 同步(sync)
操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。
如果是 异步(async)
操作,当前线程会直接往下执行,它不会阻塞当前线程。
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
关于同步异步、串行并行和线程的关系,如下表格所示
同步 异步 主队列 在主线程中执行 在主线程中执行 串行队列 在当前线程中执行 新建线程执行 并发队列 在当前线程中执行 新建线程执行
5.线程状态
一般来说,线程有五个状态
- 新建状态:线程通过各种方式在被创建之初,还没有调用开始执行方法,这个时候的线程就是新建状态;
self.thread=[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
- 就绪状态:在新建线程被创建之后调用了开始执行方法
[thread start]
,但是CPU并不是真正的同时执行多个任务,所以要等待CPU调用,这个时候线程处于就绪状态。处于就绪状态的线程并不一定立即执行线程里的代码,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。
- 运行状态:线程获得CPU时间,被CPU调用之后就进入运行状态
- CPU 负责调度可调度线程池中线程的执行
- 线程执行完成之前(死亡之前),状态可能会在就绪和运行之间来回切换
- 就绪和运行之间的状态变化由 CPU 负责,程序员不能干预
- 阻塞状态:所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
- 线程通过调用sleep方法进入睡眠状态
[NSThread sleepForTimeInterval:2.0];
- 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者
- 线程试图得到一个锁,而该锁正被其他线程持有;
- 线程在等待某个触发条件
- 死亡状态:当线程的任务结束,发生异常,或者是强制退出这三种情况会导致线程的死亡。线程死亡后,线程对象从内存中移除。一旦线程进入死亡状态,再次尝试重新开启线程,则程序会挂。
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1.创建线程--->新建状态
self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(therad) object:nil];
// 设置线程名称
self.thread.name = @"线程A";
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 2.开启线程---->就绪和运行状态
[self.thread start];
}
-(void)therad {
NSThread *current=[NSThread currentThread];
NSLog(@"therad---打印线程---%@",self.thread.name);
NSLog(@"therad---线程开始---%@",current.name);
//3.设置线程阻塞1,阻塞2秒 --->阻塞状态
NSLog(@"接下来,线程阻塞2秒");
[NSThread sleepForTimeInterval:2.0];
//第二种设置线程阻塞2,以当前时间为基准阻塞4秒
NSLog(@"接下来,线程阻塞4秒");
NSDate *date=[NSDate dateWithTimeIntervalSinceNow:4.0];
[NSThread sleepUntilDate:date];
for (int i=0; i<10; i++) {
NSLog(@"线程--%d--%@",i,current.name);
// 结束线程
[NSThread exit];
}
// 4. 任务结束 --- >死亡状态
NSLog(@"test---线程结束---%@",current.name);
}
@ end
6.多线程方案
|多线程实现方案| 特点 | 语言|频率|线程生命周期|
|:------|:----:| :-:|
|pthread | 1、一套通用的多线程API 2、适用于Unix\Linux\Windows等系统 3、跨平台\可移植 4、使用难度大 | c语言 |几乎不用|由程序员进行管理|
|NSThread | 1、使用更加面向对象 2、简单易用,可直接操作线程对象 | OC语言 |偶尔使用|由程序员进行管理|
|GCD | 1、旨在替代NSThread等线程技术 2、充分利用设备的多核(自动) | c语言 |经常使用|自动管理|
|NSOperation | 1、基于GCD 2、比GCD多了一些更简单实用的功能,使用更加面向对象 | OC语言 |经常使用|自动管理|
二、GCD
1. GCD简介
全称是Grand Central Dispatch,可译为“牛逼的中枢调度器”
纯C语言,提供了非常多强大的函数
2.GCD的优势
GCD是苹果公司为多核的并行运算提出的解决方案
GCD会自动利用更多的CPU内核(比如双核、四核)
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
3.任务和队列
GCD有两大重要概念,分别是队列和任务
队列:这里的队列指任务队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务
任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在GCD中是放在block中的。
4.任务的管理(调度)
在GCD中有两种队列:串行队列和并发队列,负责任务的管理
注意:队列只负责任务的调度,不负责任务的执行-----这是理解不同函数,不同队列,任务是否在新的线程执行的关键
-
并发队列(Concurrent Dispatch Queue):只要有空闲的线程,队列就会调度当前任务,交给线程去执行,不需要考虑前面是都有任务在执行,只要有线程可以利用,队列就会调度任务。
// 并发队列的创建方法 dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT); // 使用全局并发队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
并发功能只有在异步(dispatch_async)函数下才有效,在同步函数下失去了任务调度的并发能力
-
串行队列(Serial Dispatch Queue):任务按照顺序被调度,前一个任务不执行完毕,队列不会调度,这样任务,都是一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
// 串行队列的创建方法 dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL); // 使用主队列(跟主线程相关联的队列) // 主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行 dispatch_queue_t queue = dispatch_get_main_queue();
5.任务的执行
- (1)用同步的方式执行任务 dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
参数说明:
queue:队列
block:任务
- (2)用异步的方式执行任务 dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
任务的执行方式是由函数决定的(async和sync),任务是封装在block里。
// 同步执行任务创建方法
dispatch_sync(queue, ^{
NSLog(@"%@",[NSThread currentThread]); // 这里放任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
NSLog(@"%@",[NSThread currentThread]); // 这里放任务代码
});
6.同步,异步,并发,串行的组合
sync和async:
同步函数和异步函数:负责任务的执行,决定了任务的执行方式。
同步函数(sync):只能在当前线程中执行任务,不具备开启新线程的能力
异步执行(async):可以在新的线程中执行任务,具备开启新线程的能力
serial和concurrent:
串行队列和并行队列:只负责任务的调度,不负责任务的执行
串行队列(Serial Dispatch Queue):任务按照顺序被调度,前一个任务不执行完毕,队列不会调度,这样任务,都是一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Dispatch Queue):只要有空闲的线程,队列就会调度当前任务,交给线程去执行,不需要考虑前面是都有任务在执行,只要有线程可以利用,队列就会调度任务。
各种组合的效果
并发队列 | 创建串行队列 | 主队列 | |
---|---|---|---|
同步(sync) | 1、没有开启线程 2、串行执行任务 | 1、没有开启线程 2、串行执行任务 | 1、没有开启线程 2、串行执行任务 |
异步(async) | 1、有开启线程 2、并发执行任务 | 1、有开启线程 2、串行执行任务 | 1、没有开启线程 2、串行执行任务 |
代码示范:
- (1)用异步函数往并发队列中添加任务
/**
并发队列+异步函数
*/
- (void)concurrentQueueInAsyn {
//1.获得全局的并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//2.添加任务到队列中,就可以执行任务
//异步函数:具备开启新线程的能力
dispatch_async(queue, ^{
NSLog(@"下载图片1----%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片3----%@",[NSThread currentThread]);
});
//打印主线程
NSLog(@"主线程----%@",[NSThread mainThread]);
}
打印结果:同时开启三个子线程
2017-04-17 16:30:45.518 GCD任务的执行[1343:148362] 主线程----<NSThread: 0x6100000696c0>{number = 1, name = main}
2017-04-17 16:30:45.518 GCD任务的执行[1343:148619] 下载图片2----<NSThread: 0x608000070200>{number = 4, name = (null)}
2017-04-17 16:30:45.518 GCD任务的执行[1343:148618] 下载图片1----<NSThread: 0x60800006cb40>{number = 3, name = (null)}
2017-04-17 16:30:45.518 GCD任务的执行[1343:148650] 下载图片3----<NSThread: 0x61000006e000>{number = 5, name = (null)}
- (2)用异步函数往串行队列中添加任务
/**
串行队列+异步函数
*/
- (void)serialQueueInAsyn {
//打印主线程
NSLog(@"主线程----%@",[NSThread mainThread]);
//1.创建串行队列
dispatch_queue_t queue= dispatch_queue_create("11", NULL);
//第一个参数为串行队列的名称,是c语言的字符串
//第二个参数为队列的属性,一般来说串行队列不需要赋值任何属性,所以通常传空值(NULL)
//2.添加任务到队列中执行
dispatch_async(queue, ^{
NSLog(@"下载图片1----%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载图片3----%@",[NSThread currentThread]);
});
//3.释放资源
//dispatch_release(queue);
}
打印结果:会开启线程,但是只开启一个线程
2017-04-17 16:41:02.665 GCD任务的执行[1359:153689] 主线程----<NSThread: 0x610000069a80>{number = 1, name = main}
2017-04-17 16:41:02.665 GCD任务的执行[1359:153728] 下载图片1----<NSThread: 0x610000072ec0>{number = 3, name = (null)}
2017-04-17 16:41:02.705 GCD任务的执行[1359:153728] 下载图片2----<NSThread: 0x610000072ec0>{number = 3, name = (null)}
2017-04-17 16:41:02.705 GCD任务的执行[1359:153728] 下载图片3----<NSThread: 0x610000072ec0>{number = 3, name = (null)}
- (3)用同步函数往并发队列中添加任务
/**
异步队列+同步函数
*/
- (void)concurrentQueueInSyn {
//打印主线程
NSLog(@"主线程----%@",[NSThread mainThread]);
//1.创建并行队列
dispatch_queue_t queue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//2.添加任务到队列中执行
dispatch_sync(queue, ^{
NSLog(@"下载图片1----%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片3----%@",[NSThread currentThread]);
});
}
打印结果:不会开启新的线程,并发队列失去了并发的功能
2017-04-17 16:47:17.868 GCD任务的执行[1374:157934] 主线程----<NSThread: 0x618000070e80>{number = 1, name = main}
2017-04-17 16:47:17.868 GCD任务的执行[1374:157934] 下载图片1----<NSThread: 0x618000070e80>{number = 1, name = main}
2017-04-17 16:47:17.868 GCD任务的执行[1374:157934] 下载图片2----<NSThread: 0x618000070e80>{number = 1, name = main}
2017-04-17 16:47:17.868 GCD任务的执行[1374:157934] 下载图片3----<NSThread: 0x618000070e80>{number = 1, name = main}
- (4)用同步函数往串行队列中添加任务
/**
串行队列+同步函数
*/
- (void)serialQueueInSyn {
NSLog(@"用同步函数往串行队列中添加任务");
//打印主线程
NSLog(@"主线程----%@",[NSThread mainThread]);
//创建串行队列
dispatch_queue_t queue= dispatch_queue_create("11", NULL);
//2.添加任务到队列中执行
dispatch_sync(queue, ^{
NSLog(@"下载图片1----%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片2----%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"下载图片3----%@",[NSThread currentThread]);
});
}
打印结果:不会开启新的线程
2017-04-17 16:50:19.439 GCD任务的执行[1388:160753] 主线程----<NSThread: 0x610000065600>{number = 1, name = main}
2017-04-17 16:50:19.483 GCD任务的执行[1388:160753] 下载图片1----<NSThread: 0x610000065600>{number = 1, name = main}
2017-04-17 16:50:19.483 GCD任务的执行[1388:160753] 下载图片2----<NSThread: 0x610000065600>{number = 1, name = main}
2017-04-17 16:50:19.483 GCD任务的执行[1388:160753] 下载图片3----<NSThread: 0x610000065600>{number = 1, name = main}
7.GCD其他函数
7.1 延迟执行
在3s后,可能不仅限于3s,执行处理,因为Main Dispatch Queue 在主线程的Runloop中执行,所以在比如每隔1/60秒执行的Runloop中,Block最快在3s后执行,最慢在3s+1/60s后执行,并且在Main Dispatch Queue有大量处理追加或者主线程的处理本身有延迟时,这个时间会更长,所以有严格时间的要求下使用时会出现问题
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// to do
});
7.2 一次性代码
使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
整个程序运行过程中,只会执行一次。
7.3 队列组
有这么1种需求:
首先:分别异步执行2个耗时的操作
其次:等2个异步操作都执行完毕后,再回到主线程执行操作
如果想要快速高效地实现上述需求,可以考虑用队列组
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步操作都执行完毕后,回到主线程...
});
** 需求:从网络上下载两张图片,把两张图片合并成一张最终显示在view上。**
- 1.没有使用group的方法
/**
没有使用group的方法
*/
- (void)nonGroup {
dispatch_async(global_quque, ^{
//下载图片1
UIImage *image1= [self imageWithUrl:self.imageString1];
NSLog(@"图片1下载完成---%@",[NSThread currentThread]);
//下载图片2
UIImage *image2= [self imageWithUrl:self.imageString2];
NSLog(@"图片2下载完成---%@",[NSThread currentThread]);
//回到主线程显示图片
dispatch_async(main_queue, ^{
NSLog(@"显示图片---%@",[NSThread currentThread]);
self.imageView1.image=image1;
self.imageView2.image=image2;
//合并两张图片
UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 100), NO, 0.0);
[image1 drawInRect:CGRectMake(0, 0, 100, 100)];
[image2 drawInRect:CGRectMake(100, 0, 100, 100)];
self.imageView3.image=UIGraphicsGetImageFromCurrentImageContext();
//关闭上下文
UIGraphicsEndImageContext();
NSLog(@"图片合并完成---%@",[NSThread currentThread]);
});
});
}
//封装一个方法,传入一个url参数,返回一张网络上下载的图片
- (UIImage *)imageWithUrl:(NSString *)urlStr {
NSURL *url=[NSURL URLWithString:urlStr];
NSData *data=[NSData dataWithContentsOfURL:url];
UIImage *image=[UIImage imageWithData:data];
return image;
}
打印结果:这种方式的效率不高,需要等到图片1.图片2都下载完成后才行。
2017-04-17 18:14:05.716 dispatch_group_asyn[1573:207552] 图片1下载完成---<NSThread: 0x61800006b140>{number = 5, name = (null)}
2017-04-17 18:14:05.795 dispatch_group_asyn[1573:207552] 图片2下载完成---<NSThread: 0x61800006b140>{number = 5, name = (null)}
2017-04-17 18:14:05.795 dispatch_group_asyn[1573:206907] 显示图片---<NSThread: 0x610000064ac0>{number = 1, name = main}
2017-04-17 18:14:05.802 dispatch_group_asyn[1573:206907] 图片合并完成---<NSThread: 0x610000064ac0>{number = 1, name = main}
- 2.使用group的方法
- (void)group {
//1.创建一个队列组
dispatch_group_t group = dispatch_group_create();
//2.开启一个任务下载图片1
__block UIImage *image1=nil;
dispatch_group_async(group, global_quque, ^{
image1= [self imageWithUrl:@"http://d.hiphotos.baidu.com/baike/c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=2b9a12172df5e0fefa1581533d095fcd/cefc1e178a82b9019115de3d738da9773912ef00.jpg"];
NSLog(@"图片1下载完成---%@",[NSThread currentThread]);
});
//3.开启一个任务下载图片2
__block UIImage *image2=nil;
dispatch_group_async(group, global_quque, ^{
image2= [self imageWithUrl:@"http://h.hiphotos.baidu.com/baike/c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=f47fd63ca41ea8d39e2f7c56f6635b2b/1e30e924b899a9018b8d3ab11f950a7b0308f5f9.jpg"];
NSLog(@"图片2下载完成---%@",[NSThread currentThread]);
});
//同时执行下载图片1\下载图片2操作
//4.等group中的所有任务都执行完毕, 再回到主线程执行其他操作
dispatch_group_notify(group,main_queue, ^{
NSLog(@"显示图片---%@",[NSThread currentThread]);
self.imageView1.image=image1;
self.imageView2.image=image2;
//合并两张图片
//注意最后一个参数是浮点数(0.0),不要写成0。
UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 100), NO, 0.0);
[image1 drawInRect:CGRectMake(0, 0, 100, 100)];
[image2 drawInRect:CGRectMake(100, 0, 100, 100)];
self.imageView3.image=UIGraphicsGetImageFromCurrentImageContext();
//关闭上下文
UIGraphicsEndImageContext();
NSLog(@"图片合并完成---%@",[NSThread currentThread]);
});
}
打印结果:同时开启了两个子线程,分别下载图片
2017-04-17 18:18:07.222 dispatch_group_asyn[1586:210326] 图片2下载完成---<NSThread: 0x610000077280>{number = 4, name = (null)}
2017-04-17 18:18:07.271 dispatch_group_asyn[1586:210307] 图片1下载完成---<NSThread: 0x610000077e80>{number = 5, name = (null)}
2017-04-17 18:18:07.271 dispatch_group_asyn[1586:210270] 显示图片---<NSThread: 0x618000073240>{number = 1, name = main}
2017-04-17 18:18:07.278 dispatch_group_asyn[1586:210270] 图片合并完成---<NSThread: 0x618000073240>{number = 1, name = main}
7.4 快速迭代dispatch_apply函数
- 普通迭代
/**
* 普通迭代
*/
- (void)commonIteration {
for (int i = 0; i < 9; i++) {
NSLog(@"%zd----%@", i, [NSThread currentThread]);
}
}
打印结果:速度慢
2017-04-17 23:10:26.475 dispatch_apply函数[575:8754] 0----<NSThread: 0x608000079b40>{number = 1, name = main}
2017-04-17 23:10:26.475 dispatch_apply函数[575:8754] 1----<NSThread: 0x608000079b40>{number = 1, name = main}
2017-04-17 23:10:26.475 dispatch_apply函数[575:8754] 2----<NSThread: 0x608000079b40>{number = 1, name = main}
2017-04-17 23:10:26.476 dispatch_apply函数[575:8754] 3----<NSThread: 0x608000079b40>{number = 1, name = main}
2017-04-17 23:10:26.476 dispatch_apply函数[575:8754] 4----<NSThread: 0x608000079b40>{number = 1, name = main}
2017-04-17 23:10:26.476 dispatch_apply函数[575:8754] 5----<NSThread: 0x608000079b40>{number = 1, name = main}
2017-04-17 23:10:26.476 dispatch_apply函数[575:8754] 6----<NSThread: 0x608000079b40>{number = 1, name = main}
2017-04-17 23:10:26.476 dispatch_apply函数[575:8754] 7----<NSThread: 0x608000079b40>{number = 1, name = main}
2017-04-17 23:10:26.477 dispatch_apply函数[575:8754] 8----<NSThread: 0x608000079b40>{number = 1, name = main}
- 多线程快速迭代
/**
* 快速迭代(应用:拷贝文件)
*/
- (void)quickIteration {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(9, queue, ^(size_t index) {
NSLog(@"%zd----%@", index, [NSThread currentThread]);
});
}
打印结果:创建多条线程,进行迭代,速度快
2017-04-17 23:12:39.705 dispatch_apply函数[605:11278] 0----<NSThread: 0x600000261240>{number = 1, name = main}
2017-04-17 23:12:39.705 dispatch_apply函数[605:11318] 3----<NSThread: 0x608000267040>{number = 5, name = (null)}
2017-04-17 23:12:39.705 dispatch_apply函数[605:11335] 1----<NSThread: 0x600000269f40>{number = 3, name = (null)}
2017-04-17 23:12:39.705 dispatch_apply函数[605:11315] 2----<NSThread: 0x6080002668c0>{number = 4, name = (null)}
2017-04-17 23:12:39.705 dispatch_apply函数[605:11278] 4----<NSThread: 0x600000261240>{number = 1, name = main}
2017-04-17 23:12:39.705 dispatch_apply函数[605:11318] 5----<NSThread: 0x608000267040>{number = 5, name = (null)}
2017-04-17 23:12:39.705 dispatch_apply函数[605:11335] 6----<NSThread: 0x600000269f40>{number = 3, name = (null)}
2017-04-17 23:12:39.705 dispatch_apply函数[605:11315] 7----<NSThread: 0x6080002668c0>{number = 4, name = (null)}
2017-04-17 23:12:39.705 dispatch_apply函数[605:11278] 8----<NSThread: 0x600000261240>{number = 1, name = main}
7.5.dispatch_barrier_async
在访问数据库或文件时,使用Serial Dispatch Queue 可避免数据竞争的问题。
写入处理确实不可与其他的写入处理以及包含读取处理的其他某些处理并行执行。但是如果读取处理只是读取处理并行执行,那么多个并行执行就不会发生问题。
也就是说,为了高效率的进行访问,读取处理追加到Concurrent Dispatch Queue中,写入处理在任何一个读取处理没有执行的状态下,追加到Serial Dispatch Queue中即可(在写入处理之前,读取处理不可执行)
/**
并发队列+异步函数 读1和读2之间写入数据
*/
- (void)concurrentQueueInAsync {
dispatch_queue_t queue = dispatch_queue_create("JJ", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"block for reading 0");
});
dispatch_async(queue, ^{
NSLog(@"block for reading 1");
});
// 在1和2之间执行写入处理,那么根据Concurrent Dispatch Queue的性质,就有可能在追加到写入处理前面的处理中读取到与期待不符的数据。
dispatch_async(queue, ^{
NSLog(@"block for writing");
});
dispatch_async(queue, ^{
NSLog(@"block for reading 2");
});
dispatch_async(queue, ^{
NSLog(@"block for reading 3");
});
}
因此我们要使用dispatch_barrier_async函数。dispatch_barrier_async函数会等待追加到Concurrent Dispatch Queue上的并行执行的处理全部结束之后,再将指定的处理追加到该Concurrent Dispatch Queue中,然后在由dispatch_barrier_async函数追加的处理执行完毕后,Concurrent Dispatch Queue才恢复为一般的动作,追加到该Concurrent Dispatch Queue的处理又开始并行执行
/**
dispatch_barrier_async函数控制读写操作 读1和读2之间写入数据
*/
- (void)dispatch_barrier_async {
dispatch_queue_t queue = dispatch_queue_create("JJ", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"block for reading 0");
});
dispatch_async(queue, ^{
NSLog(@"block for reading 1");
});
// 将这个block之前的任务拦着,等执行完后,执行这个block,然后再并行执行其他任务。
dispatch_barrier_async(queue, ^{
NSLog(@"block for writing");
});
dispatch_async(queue, ^{
NSLog(@"block for reading 2");
});
dispatch_async(queue, ^{
NSLog(@"block for reading 3");
});
}
使用Concurrent Dispatch Queue和dispatch_barrier_async函数可以实现高效率的数据库访问和文件访问。
demo地址
7.6.补充
-
dispatch_sync函数
将指定的block任务同步追加到指定的Dispatch Queue中,在追加Block结束之前,dispatch_sync函数会一直等待,产生死锁问题
在主线程中执行以下代码会死锁。
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"block middle");
});
a.dispatch_sync()函数在主线程中调用;
b.调用dispatch_sync()函数会立即阻塞调用时该函数所在的线程,即主线程等待dispatch_sync()函数返回
c.dispatch_sync()函数追加任务(即Block代码块)到主队列dispatch_get_main_queue()中
d.主队列是一种特殊的串行队列,主队列的任务在主线程中执行,但此时主线程被阻塞,无法执行Block代码块,导致dispatch_sync()函数无法返回,一直等待Block被主线程执行,最终导致死锁
也就是说,主线程等待dispatch_sync函数返回,而dispatch_sync函数将block添加到主线程,主线程因为要等dispatch_sync函数返回才去执行block,主线程被阻塞,没有机会去执行block,发生死锁。
解决上述问题的死锁,也简单,使用Global Dispatch Queue进行处理block任务
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
NSLog(@"block middle");
});
a.调用dispatch_sync()函数会立即阻塞调用时该函数所在的线程,全局并发线程等待dispatch_sync()函数返回
b.dispatch_sync()函数追加任务(即Block代码块)到并发队列dispatch_get_global_queue()中
c.并发队列dispatch_get_global_queue()并发执行任务,block不需等待,可以执行,执行完后,dispatch_sync函数返回
三、NSOperation和NSOperationQueue
- NSOperation的作用:配合使用NSOperation和NSOperationQueue也能实现多线程编程
- NSOperation和NSOperationQueue实现多线程的具体步骤
- 先将需要执行的操作封装到一个NSOperation对象中
- 然后将NSOperation对象添加到NSOperationQueue中
- 系统会自动将NSOperationQueue中的NSOperation取出来,放到线程执行
1.NSOperation
NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类:
- NSInvocationOperation
- NSBlockOperation
- 自定义子类继承NSOperation,实现内部相应的方法
1.1NSInvocationOperation
a.创建NSInvocationOperation对象
-(id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;
b.调用start方法开始执行操作
- (void)start;
注意:默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作,只有将NSOperation放到一个NSOperationQueue中,才会异步执行操作
- 代码
/**
NSInvocationOperation的使用
*/
- (void)invocationOperation {
// 1.创建操作
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
// 2.启动操作
[op start];
}
- (void)run {
NSLog(@"------%@", [NSThread currentThread]);
}
- 打印结果:不开启线程,在当前线程执行操作
2017-04-17 23:27:16.796 NSOperation子类的基本使用[845:19316] ------<NSThread: 0x600000076b00>{number = 1, name = main}
1.2.NSBlockOperation
a.创建NSBlockOperation对象
+(id)blockOperationWithBlock:(void (^)(void))block;
b.通过addExecutionBlock:方法添加更多的操作
-(void)addExecutionBlock:(void (^)(void))block;
c.调用start方法开始执行操作
- (void)start;
注意:只要NSBlockOperation封装的操作数 > 1,就会异步执行操作
- 代码
/**
NSBlockOperation的基本使用
*/
- (void)blockOperation {
// 1.创建操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
// 在主线程
NSLog(@"下载1------%@", [NSThread currentThread]);
}];
// 2.添加额外的任务(在子线程执行)
[op addExecutionBlock:^{
NSLog(@"下载2------%@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"下载3------%@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"下载4------%@", [NSThread currentThread]);
}];
// 3.启动操作
[op start];
}
- 打印结果:第一个任务在当前当前线程,后面添加的任务会放在新的线程里执行
2017-04-17 23:30:54.619 NSOperation子类的基本使用[872:21229] 下载1------<NSThread: 0x6080000645c0>{number = 1, name = main}
2017-04-17 23:30:54.619 NSOperation子类的基本使用[872:21267] 下载3------<NSThread: 0x600000071c80>{number = 4, name = (null)}
2017-04-17 23:30:54.619 NSOperation子类的基本使用[872:21266] 下载2------<NSThread: 0x60800006da80>{number = 3, name = (null)}
2017-04-17 23:30:54.619 NSOperation子类的基本使用[872:21269] 下载4------<NSThread: 0x600000071dc0>{number = 5, name = (null)}
1.3.自定义NSOperation
a.自定义NSOperation的步骤
- 继承NSOperation
- 重写- (void)main方法,在里面实现想执行的任务
- 外部创建操作,并调用start方法
#import "CustomOperation.h"
@implementation CustomOperation
/**
重新main方法,在这里执行任务
*/
- (void)main {
for (NSInteger i = 0; i<10; i++) {
NSLog(@"download1 -%zd-- %@", i, [NSThread currentThread]);
}
if (self.isCancelled) return;
for (NSInteger i = 0; i<10; i++) {
NSLog(@"download2 -%zd-- %@", i, [NSThread currentThread]);
}
if (self.isCancelled) return;
for (NSInteger i = 0; i<10; i++) {
NSLog(@"download3 -%zd-- %@", i, [NSThread currentThread]);
}
if (self.isCancelled) return;
}
/**
自定义操作的使用
*/
- (void)customOperation {
CustomOperation *op = [CustomOperation new];
[op start];
}
1.4.操作之间的关系 -- 操作依赖
NSOperation之间可以轻松的设置依赖来保证执行顺序
[operationB addDependency:operationA]; // 操作B依赖于操作A
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"download1----%@", [NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"download2----%@", [NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"download3----%@", [NSThread currentThread]);
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"download4----%@", [NSThread currentThread]);
}];
NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"download5----%@", [NSThread currentThread]);
}];
// 设置依赖
[op3 addDependency:op1];
[op3 addDependency:op2];
[op3 addDependency:op4];
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
[queue addOperation:op4];
[queue addOperation:op5];
打印结果:操作3一定是在操作1,2,4执行完后才执行
2017-04-18 10:55:14.185 OperationDependency[2914:60630] download1----<NSThread: 0x61000007dc00>{number = 3, name = (null)}
2017-04-18 10:55:14.185 OperationDependency[2914:60633] download4----<NSThread: 0x60000007d840>{number = 5, name = (null)}
2017-04-18 10:55:14.185 OperationDependency[2914:60634] download5----<NSThread: 0x60000007c900>{number = 6, name = (null)}
2017-04-18 10:55:14.185 OperationDependency[2914:60632] download2----<NSThread: 0x618000267c40>{number = 4, name = (null)}
2017-04-18 10:55:14.189 OperationDependency[2914:60633] download3----<NSThread: 0x60000007d840>{number = 5, name = (null)}
1.5操作的监听
NSOperation可以很容易的设置监听任务的完成,可以监听一个操作的执行完毕
-(void (^)(void))completionBlock;
-(void)setCompletionBlock:(void (^)(void))block;
2、NSOperationQueue
NSOperation可以调用start方法来执行任务,但默认是同步执行的
如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作
使用NSOperationQueue分两步,队列会根据系统情况,自行决定是否开启线程
- 创建操作
- 将操作添加到队列中
2.1 添加操作到NSOperationQueue中
-(void)addOperation:(NSOperation *)op;
-(void)addOperationWithBlock:(void (^)(void))block;
- 代码
/**
队列添加操作
*/
- (void)queueAddOperation {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作(操作把任务封装起来)
// 设置最大并发操作数
// queue.maxConcurrentOperationCount = 2;
//queue.maxConcurrentOperationCount = 1; // 就变成了串行队列
// 2.1 创建NSInvocationOperation
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
// 2.2 创建NSBlockOperation
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"download3 --- %@", [NSThread currentThread]);
}];
[op3 addExecutionBlock:^{
NSLog(@"download4 --- %@", [NSThread currentThread]);
}];
[op3 addExecutionBlock:^{
NSLog(@"download5 --- %@", [NSThread currentThread]);
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"download6 --- %@", [NSThread currentThread]);
}];
// 2.3 创建CustomOperation
CustomOperation *op5 = [[CustomOperation alloc] init];
// 3. 添加操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
[queue addOperation:op4];
[queue addOperation:op5];
}
- (void)download1 {
NSLog(@"download1 --- %@", [NSThread currentThread]);
}
- (void)download2 {
NSLog(@"download2 --- %@", [NSThread currentThread]);
}
- 打印结果
2017-04-17 23:43:00.974 NSOperation与NSOperationQueue初步使用[922:27537] download3 --- <NSThread: 0x60000026f040>{number = 6, name = (null)}
2017-04-17 23:43:00.974 NSOperation与NSOperationQueue初步使用[922:27518] download2 --- <NSThread: 0x608000273400>{number = 3, name = (null)}
2017-04-17 23:43:00.974 NSOperation与NSOperationQueue初步使用[922:27517] download1 --- <NSThread: 0x600000267180>{number = 4, name = (null)}
2017-04-17 23:43:00.974 NSOperation与NSOperationQueue初步使用[922:27520] download6 --- <NSThread: 0x60000026ea00>{number = 5, name = (null)}
2017-04-17 23:43:00.974 NSOperation与NSOperationQueue初步使用[922:27680] download4 --- <NSThread: 0x608000273040>{number = 7, name = (null)}
2017-04-17 23:43:00.974 NSOperation与NSOperationQueue初步使用[922:27537] download5 --- <NSThread: 0x60000026f040>{number = 6, name = (null)}
2.2 最大并发数:同时执行的任务数
队列是串行还是并行,可以通过控制最大并发数决定
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;
// 设置最大并发操作数
queue.maxConcurrentOperationCount = 1; // 就变成了串行队列
2.3 队列的取消、暂停、恢复
和GCD一样,都可以设置队列的取消,暂停和恢复
- (void)cancelAllOperations;
- (void)setSuspended:(BOOL)b;
四、补充
1.GCD和NSOperation的区别
GCD的队列类型
- 并发队列
- 自己创建的
- 全局
- 串行队列
- 主队列
- 自己创建的
NSOperationQueue的队列类型
- 主队列
- [NSOperationQueue mainQueue]
- 凡是添加到主队列中的任务(NSOperation),都会放到主线程中执行
- 非主队列(其他队列)
- [[NSOperationQueue alloc] init]
- 同时包含了:串行、并发功能,可以通过设置最大并发数控制是串行还是并发队列
- 添加到这种队列中的任务(NSOperation),就会自动放到子线程中执行
主要区别:
- GCD是纯C语言的API,NSOperation是基于GCD的OC版本封装
- GCD只支持FIFO的队列,NSOperation可以很方便地调整执行顺序,设置最大并发数量
- NSOperationQueue可以轻松在operation间设置依赖关系,而GCD需要些很多代码才能实现
- NSOperationQueue支持KVO,可以检测operation是否正在执行(isExecuted),是否结束(isFinisn),是否取消(isCancel)
- GCD的执行速度比NSOperation快
2.线程间的通信
在iOS开发过程中,我们一般在主线程里边进行UI刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
-
NSThread
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO]
GCD
/**
线程之间的通信
*/
- (void)connectionBetweenThread {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 图片的网络路径
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
// 加载图片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成图片
UIImage *image = [UIImage imageWithData:data];
// 回到主线程
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
}
- NSOperationQueue
/**
* 线程之间的通信
*/
- (void)connectionBetweenThread {
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
// 图片的网络路径
NSURL *url = [NSURL URLWithString:@"http://img.pconline.com.cn/images/photoblog/9/9/8/1/9981681/200910/11/1255259355826.jpg"];
// 加载图片
NSData *data = [NSData dataWithContentsOfURL:url];
// 生成图片
UIImage *image = [UIImage imageWithData:data];
// 回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
}
线程安全
-
1.多线程安全隐患引出
假设火车站有3个卖票窗口,余票是1000,卖票窗口3个线程同一时刻读取剩余票数,都是读取的1000,卖票线程1卖了一张 ,余票变成999。卖票线程2反应慢点,在卖票线程1后面执行卖票,因为卖票线程2刚开始读取的余票也是1000,所以在卖掉一张后,余额也变成999。卖票线程3反应更慢,在卖票线程2后面执行卖票,因为卖票线程3刚开始读取的余票也是1000,所以在卖掉一张后,余额依旧也变成999。所以出现了错误,本来卖了3张,可是余票还有999张。
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题
比如多个线程访问同一个对象、同一个变量、同一个文件,并对这一块资源进行读写操作时,容易发生问题
-
2、多线程安全隐患代码示例
// 创建线程
// 创建3条线程 - (void)viewDidLoad { [super viewDidLoad]; self.ticketCount = 100; self.thread01 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil]; self.thread01.name = @"售票员01"; self.thread02 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil]; self.thread02.name = @"售票员02"; self.thread03 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil]; self.thread03.name = @"售票员03";
}
```
// 3条线程访问资源
// 3条线程访问资源
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.thread01 start];
[self.thread02 start];
[self.thread03 start];
}
// 卖票方法
- (void)saleTicket {
while (1) {
//@synchronized(self) {
// 先取出总数
NSInteger count = self.ticketCount;
if (count > 0) {
self.ticketCount = count - 1;
NSLog(@"%@卖了一张票,还剩下%zd张", [NSThread currentThread].name, self.ticketCount);
} else {
NSLog(@"票已经卖完了");
break;
}
// }
}
}
-
3.多线程安全隐患解决方案
在线程A读取数据后,加一把锁,别的线程就不能访问了,只允许加锁的线程A访问。当这个加锁线程操作完数据后,线程A解锁,此时别的线程就能访问了, 假设轮到线程B访问,线程B也先加把锁,保证只有自己能访问,执行完操作再解锁....就这样循环往复,只要谁访问就加把锁,直到操作结束后再解锁。这就是互斥锁。
注意:
1.尽管互斥锁能够有效防止因多线程抢夺资源而造成的数据安全问题,但是其需要消耗大量的CPU资源;
2.而且只有再多条线程抢夺同一块资源的时候才使用互斥锁
-
4.熟悉SDWebImage的实现原理
以SDWebImage的实现原理为demo例子,熟悉如何自定义NSOperation, 如何使用NSOperationQueue以及线程之间如何通信,大家自己去理解(这个demo我对这位作者文章的学习模仿)
模仿SDWebImage的轻量图片异步下载框架BBSDWebImage