IOS多线程总结

目录

  • 简述
  • NSThread
  • GCD
    • 操作与队列
      • 异步操作并行队列
      • 同步操作并行队列
      • 同步操作串行队列
      • 异步操作串行队列
    • 队列创建
      • 新建队列
      • 系统提供
    • dispatch_set_target_queue
    • dispatch_after
    • dispatch Group
    • dispatch_barrier_async
  • NSOperation
    • NSOperationQueue
    • 添加依赖
    • 其他用法
  • 优劣比较
  • 常见并发问题及其解决方案
    • 死锁
    • 数据竞争
    • 同步执行

简述

在学习多线程之前,我们先了解线程是什么。线程通俗的理解就是一条单车道。例如下面这个helloworld

NSString *str=@"Hello, World!";
        NSLog(@"%@",str);

这段OC代码在经过编译器以后,会转换成二进制代码。这些二进制代码会被机器一条一条执行。由于CPU一次只能执行一个命令(单车道),因此,我们将CPU执行CPU命令列(二进制代码)的路径称之为线程

光有单车道可不行,那样会堵死的,我们需要双车道乃至三四五六七八车道,这就是多线程。无论CPU再怎么厉害,它每次执行的命令只能有一个,就像我道路的宽度已经被规定为一辆小汽车的宽度,那我们在不扩宽道路的情况下要容纳更多的汽车,只能再加几条道路。

OS X和iOS的核心XNU内核在发生系统操作事件时会切换执行路径。执行中的路径状态,例如CPU的寄存器等信息保存到各自路径专用的内存块中,从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令列,这被称之为“上下文切换”

多线程的实现原理就是基于线程之间反复多次的上下文切换,下面就是iOS开发中比较常见的多线程技术。

NSThread

NSThread是苹果提供的一种轻量级的线程方法,因为它的轻量,所以线程的生命周期、同步、加锁问题都需要自己手动管理。一般像获取当前线程、让程序回到主线程更新UI,用NSThread封装的方法比较方便。

使用方法

创建&开启

//创建线程,里面包含run方法
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];  
// 设置线程的优先级(0.0 - 1.0,1.0最高级)  
thread.threadPriority = 1;  
// 开启线程  
[thread start]; 

获取&暂停线程

//获取当前线程
NSThread *current = [NSThread currentThread];
//获取主线程
NSThread *main = [NSThread mainThread];
// 暂停2s  
[NSThread sleepForTimeInterval:2];  
  
// 或者  
NSDate *date = [NSDate dateWithTimeInterval:2 sinceDate:[NSDate date]];  
[NSThread sleepUntilDate:date];

线程通信

//在指定线程执行操作
[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:YES];
//在主线程执行操作
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES]; 
//在当前线程执行操作
[self performSelector:@selector(run) withObject:nil];

GCD

什么是GCD,GCD(grand central dispatch)是苹果提供的异步执行任务技术之一。一般将应用程序中技术的线程管理用的代码在系统级实现。开发者只需要定义想执行的任务并追加到适当的dispatch queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前的线程更有效率。另外在性能方面,GCD的线程管理是系统级的,也就是在XNU内核上实现的,因此无论我们再怎么编,再怎么使用pthread(C语言的线程框架)和NSThread,性能都不可能超过GCD,因此如果需要统一线程管理,首选GCD。

操作与队列

GCD是一个很大的模块,因此我先从操作与队列开始讲起,只是看使用的话,看这一点也差不多了。在GCD中,有4个容易混淆的概念:同步、异步、并行、串行。

同步(sync) 和 异步(async) 修饰的都是操作,主要区别在于会不会阻塞当前线程,直到 Block 中的任务执行完毕!

  • 如果是 同步(sync) 操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。
  • 如果是 异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程。

并行(concurrent)和串行(serial)修饰的都是队列,主要区别在于队列里的任务是否需要等待其他任务。

  • 如果是串行(serial)队列,GCD 会 FIFO(先进先出) 地取出来一个放在队列里面的任务,执行一个,然后取下一个,这样一个一个的执行
  • 如果并行(concurrent)队列,则不会等待正在处理中的任务。

我们使用GCD,实际上就是操作(同步、异步)队列(串行、并行),因此一共有4种组合方式。他们的组合会产生怎样的效果?见下表

同步 异步
串行 当前线程,任务按顺序执行 其他某个线程,任务按顺序执行
并行 当前线程,任务按顺序执行 其他某些线程,任务一起执行

如果要简单记忆的话,可以记忆成异步一定会开启线程,并行只有遇到异步调用的时候才是真正的一起执行。而具体怎么使用看具体情况而定

  • 有次序不阻塞线程,创建串行队列,异步操作
  • 开启多个线程同时执行(不推荐,线程开销大),创建并行队列,异步操作。

下面是实现demo

异步操作并行队列

通过dispatch_queue_create方法,创建了一个名叫"test.mathewchen.queue"的队列,第二个参数是确定该队列为并行或者串行,null为串行,DISPATCH_QUEUE_CONCURRENT为并行。

dispatch_queue_t queue = dispatch_queue_create("test.mathewchen.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^(void){
        NSLog(@"block:%d,thread:%@",0,[NSThread currentThread]);
    });
......
dispatch_async(queue, ^(void){
        NSLog(@"block:%d,thread:%@",13,[NSThread currentThread]);
    });

打出的结果

2016-09-21 17:05:26.735 testGCD[4732:1575766] block:1,thread:{number = 3, name = (null)}
2016-09-21 17:05:26.735 testGCD[4732:1575774] block:2,thread:{number = 5, name = (null)}
2016-09-21 17:05:26.735 testGCD[4732:1575776] block:0,thread:{number = 2, name = (null)}
2016-09-21 17:05:26.735 testGCD[4732:1575784] block:3,thread:{number = 4, name = (null)}
2016-09-21 17:05:26.735 testGCD[4732:1575785] block:4,thread:{number = 6, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575766] block:5,thread:{number = 3, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575774] block:6,thread:{number = 5, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575776] block:7,thread:{number = 2, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575784] block:8,thread:{number = 4, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575785] block:9,thread:{number = 6, name = (null)}
2016-09-21 17:05:26.736 testGCD[4732:1575766] block:10,thread:{number = 3, name = (null)}
2016-09-21 17:05:26.737 testGCD[4732:1575774] block:11,thread:{number = 5, name = (null)}
2016-09-21 17:05:26.737 testGCD[4732:1575786] block:12,thread:{number = 7, name = (null)}
2016-09-21 17:05:26.737 testGCD[4732:1575776] block:13,thread:{number = 2, name = (null)}

很显然,当异步调用包含n个任务的并行队列的话,会创建m条线程,m<=n,具体开几个线程是根据任务的耗时定的,例如我们上面的例子,一开始先开了35246这五个线程,当执行到第六个block时,系统发现第一个线程已经完事了,复用了一开始创建的3线程,后面执行block12的时候,系统发现没有线程可以复用(name=2线程的任务没有执行完毕),于是新开了一个7线程,因为是并行队列所以前面block012顺序有点乱,因此,可以整理出线程分配表如下。

3 5 2 4 6 7
block1 block2 block0 block3 block4 复用3
block5 block6 block7 block8 block9 复用3
block10 block11 block13 block12

同步操作并行队列

现在我们看一下同步执行并行队列会打出什么LOG,代码如下

dispatch_sync(queue, ^(void){
        NSLog(@"block:%d,thread:%@",0,[NSThread currentThread]);
    });

打出的log

2016-09-22 08:53:46.173 testGCD[4879:1599485] block:0,thread:{number = 1, name = main}
2016-09-22 08:53:46.173 testGCD[4879:1599485] block:1,thread:{number = 1, name = main}
······
2016-09-22 08:53:46.187 testGCD[4879:1599485] block:13,thread:{number = 1, name = main}

很明显的看出,并行队列在同步操作中是失效的。之所以会出现这种情况,是因为GCD操作并行队列的本质所决定的。GCD在操作队列时,无论是并行还是串行,都会根据FIFO(先进先出)原则,将队列中的任务取出。区别在于,串行会放入同一个线程执行任务,而并行取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。由于同步操作是单线程的,并行队列没有其他线程可以执行任务,因此才会产生了在同步操作下,串行和并行队列没有区别。

同步操作串行队列

这个没什么好说的了,打出的LOG和同步操作并行队列一模一样。

异步操作串行行队列

代码如下

dispatch_queue_t queue = dispatch_queue_create("test.mathewchen.queue", NULL);
dispatch_async(queue, ^(void){
        NSLog(@"block:%d,thread:%@",0,[NSThread currentThread]);
    });
......
dispatch_async(queue, ^(void){
        NSLog(@"block:%d,thread:%@",13,[NSThread currentThread]);
    });

打出的log

2016-09-22 11:21:31.640 testGCD[5058:1633715] block:0,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.641 testGCD[5058:1633715] block:1,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.641 testGCD[5058:1633715] block:2,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.642 testGCD[5058:1633715] block:3,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.643 testGCD[5058:1633715] block:4,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.643 testGCD[5058:1633715] block:5,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.644 testGCD[5058:1633715] block:6,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.644 testGCD[5058:1633715] block:7,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.649 testGCD[5058:1633715] block:8,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.651 testGCD[5058:1633715] block:9,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.667 testGCD[5058:1633715] block:10,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.668 testGCD[5058:1633715] block:11,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.668 testGCD[5058:1633715] block:12,thread:{number = 2, name = (null)}
2016-09-22 11:21:31.668 testGCD[5058:1633715] block:13,thread:{number = 2, name = (null)}

可以看到,异步调用串行队列,为了不阻塞主线程,会新开一个线程。

队列的创建

在GCD中,获取队列的方式有两种,一种是新建队列,另一种是使用系统标准提供的队列。在MRC中队列使用完成还需要调用dispatch_release释放掉,而ARC中已经帮我们做了。

新建队列

在GCD中可以使用如下API进行队列的创建:

dispatch_queue_t queue = dispatch_queue_create("test.mathewchen.queue", DISPATCH_QUEUE_CONCURRENT);

第一个参数是为我们新建的队列命名,第二个参数则决定了我们该队列的性质:并行or串行。第二个参数是DISPATCH_QUEUE_SERIAL或NULL(常用)时,创建的队列为串行队列。如果为DISPATCH_QUEUE_CONCURRENT则为并行队列。

系统提供

即使我们什么也不做,系统也会给我们提供几个队列供我们使用。一种是Main Dispatch Queue,另一种则是Global Dispatch Queue。

Main Dispatch Queue就是我们常用的主线程队列,这是一个串行队列,我们想要更新UI必须在这个线程执行,可以通过该方法获取:

dispatch_get_main_queue()

Global Dispatch Queue是所有应用程序都能够使用的并行队列,创建队列是有开销的,因此一般情况下我们应该使用系统给我们提供的Global Dispatch Queue。

Global Dispatch Queue有四个优先级,分别是High Priority,Default Priority,Low Priority,Background Priority。通过creat创建的所有队列默认是Default优先级,苹果的XNU内核会管理用于Global Dispatch Queue的线程,并把Global Dispatch Queue的优先级设置成线程的优先级。获取方法如下:

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0)

dispatch_set_target_queue

上一个点有说到通过creat创建的所有队列默认是Default优先级,那我们想改变它的优先级行不行?行,用dispatch_set_target_queue,用法如下:

dispatch_queue_t serialQueue = dispatch_queue_create("com.mathewchen.queue",NULL);  
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);  
   
dispatch_set_target_queue(serialQueue, globalQueue);  
/* * 第一个参数为要设置优先级的queue,第二个参数是参照物,既将第一个queue的优先级和第二个queue的优先级设置一样。 
     */

上面是它的第一个作用,设置queue的优先级。那么第二个作用就有意思了,设置queue的层级。那我闲着没事给queue设置层级干嘛?设置层级最主要的目的是解决并发问题。

例如现在有一个任务被拆分成ABCDEF五个串行队列,但实际还是需要这个任务同步执行,那么就会有问题,因为多个串行queue之间是并行的。就像下面这个例子:

    dispatch_queue_t queue = dispatch_queue_create("test.mathewchen0.queue", NULL);
    ······
    dispatch_queue_t queue5 = dispatch_queue_create("test.mathewchen5.queue", NULL);
    dispatch_async(queue, ^{
        //code here
        NSLog(@"同步调用开始,当前线程:%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:3.f];
        NSLog(@"同步调用结束,当前线程:%@", [NSThread currentThread]);
    });
    ······
    dispatch_async(queue5, ^{
        //code here
        NSLog(@"同步调用开始5,当前线程:%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:3.f];
        NSLog(@"同步调用结束5,当前线程:%@", [NSThread currentThread]);
    });

打出的LOG如下

2016-09-26 12:26:40.846 testGCD[6593:1851518] 同步调用开始,当前线程:{number = 2, name = (null)}
2016-09-26 12:26:40.846 testGCD[6593:1851528] 同步调用开始3,当前线程:{number = 4, name = (null)}
2016-09-26 12:26:40.846 testGCD[6593:1851517] 同步调用开始1,当前线程:{number = 3, name = (null)}
2016-09-26 12:26:40.847 testGCD[6593:1851529] 同步调用开始4,当前线程:{number = 5, name = (null)}
2016-09-26 12:26:40.847 testGCD[6593:1851510] 同步调用开始2,当前线程:{number = 6, name = (null)}
2016-09-26 12:26:40.847 testGCD[6593:1851530] 同步调用开始5,当前线程:{number = 7, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851517] 同步调用结束1,当前线程:{number = 3, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851528] 同步调用结束3,当前线程:{number = 4, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851529] 同步调用结束4,当前线程:{number = 5, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851510] 同步调用结束2,当前线程:{number = 6, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851518] 同步调用结束,当前线程:{number = 2, name = (null)}
2016-09-26 12:26:43.850 testGCD[6593:1851530] 同步调用结束5,当前线程:{number = 7, name = (null)}

很显然,拆分成的六个queue并没有等待其他几个queue,全都并行执行了。解决方法如下,给所有的queue设置一个targer queue:

    dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue", NULL);
    dispatch_set_target_queue(queue, targetQueue);
    dispatch_set_target_queue(queue1, targetQueue);
    dispatch_set_target_queue(queue2, targetQueue);
    dispatch_set_target_queue(queue3, targetQueue);
    dispatch_set_target_queue(queue4, targetQueue);
    dispatch_set_target_queue(queue5, targetQueue);

这次打出的LOG就符合我们的要求了

2016-09-26 12:33:40.312 testGCD[6643:1855135] 同步调用开始,当前线程:{number = 2, name = (null)}
2016-09-26 12:33:43.316 testGCD[6643:1855135] 同步调用结束,当前线程:{number = 2, name = (null)}
2016-09-26 12:33:43.316 testGCD[6643:1855135] 同步调用开始1,当前线程:{number = 2, name = (null)}
2016-09-26 12:33:46.321 testGCD[6643:1855135] 同步调用结束1,当前线程:{number = 2, name = (null)}
2016-09-26 12:33:46.321 testGCD[6643:1855135] 同步调用开始2,当前线程:{number = 2, name = (null)}
2016-09-26 12:33:49.326 testGCD[6643:1855135] 同步调用结束2,当前线程:{number = 2, name = (null)}
2016-09-26 12:33:49.326 testGCD[6643:1855135] 同步调用开始3,当前线程:{number = 2, name = (null)}
2016-09-26 12:33:52.328 testGCD[6643:1855135] 同步调用结束3,当前线程:{number = 2, name = (null)}
2016-09-26 12:33:52.328 testGCD[6643:1855135] 同步调用开始4,当前线程:{number = 2, name = (null)}
2016-09-26 12:33:55.331 testGCD[6643:1855135] 同步调用结束4,当前线程:{number = 2, name = (null)}
2016-09-26 12:33:55.331 testGCD[6643:1855135] 同步调用开始5,当前线程:{number = 2, name = (null)}
2016-09-26 12:33:58.334 testGCD[6643:1855135] 同步调用结束5,当前线程:{number = 2, name = (null)}

dispatch_after

这个方法经常被用做延迟触发,不过一般用

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

比较多,该方法的使用如下:

dispatch_time_t time=dispatch_time(DISPATCH_TIME_NOW, 3ull*NSEC_PER_SEC);
    dispatch_after(time, queue, ^{
        NSLog(@"三秒后执行");
    });

dispatch_after方法并不是在指定时间后执行处理,只是在指定时间后把添加到queue中,queue每次循环也是有一些小小的间隔,这就会导致一些微小的误差,不过误差是毫秒级的,一般使用问题不大。如果想要使用精确时间的话,使用dispatch_walltime即可,具体使用方法就不赘述了。

Dispatch Group

假如有这么一种情况,我需要在ABCD任务执行完以后,再执行任务E。如果是串行队列的话,我直接在D中调用E就好了。那么问题来了,ABCD是并行队列的话,以哪一个任务结束作为结束点都不能很好的解决我们的问题。因此,可以将ABCD打包成一个任务,然后塞进串行队列再进行下一步处理。这是一般的解决思路,苹果觉得每一次都要这样处理太过于麻烦,于是提供了一个Dispatch Group来解决类似问题。用法如下:

dispatch_group_t group=dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        NSLog(@"block0");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"block1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"block2");
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"执行完毕");
    });

打出LOG

2016-09-27 18:14:41.265 testGCD[7034:1896349] block0
2016-09-27 18:14:41.276 testGCD[7034:1896349] block1
2016-09-27 18:14:41.277 testGCD[7034:1896349] block2
2016-09-27 18:14:41.278 testGCD[7034:1896319] 执行完毕

创建什么的跟队列差不多就不赘述了,我们将queue打包进group,当group中的任务执行完毕以后会执行notify中的代码。

最后再提一下group中比较常用的dispatch_group_wait方法,这个方法比较有意思,它是用来判断Group是否执行完毕。它有两个参数,第一个参数就是监听的group,第二个参数是超时时间。当时间超过超时时间时,该方法会返回一个非0值,代表Group中还有任务没有处理完毕,反之0就是处理完毕,可以用这个方法做一些耗时任务的判断,万一太耗时了就把它cancel掉。

dispatch_barrier_async

多线程并发问题除了死锁,还有一个数据竞争问题。最常见的就是数据库写入操作时,不可以与其他写入或读取操作一起执行。要是懒一点的程序员,直接在数据库的写入或者读取的时候上个锁就好了,让某一时间下对数据库的操作只能有一个。但是,同时读取数据库是可以的,而且为了提高读取的效率,我们最好支持同时读取。我们前面讲的group、settarget都可以解决这个问题,只是比较麻烦而已,现在来介绍另一种方法解决,那就是barrier。

举例说明一下用法。看下面代码

dispatch_async(queue,doread);
dispatch_async(queue,doread1);
dispatch_async(queue,doread2);
dispatch_async(queue,doread3);
dispatch_async(queue,doread4);
dispatch_async(queue,doread5);

我们现在有6个并行处理的读取操作,假如现在需要进行写入操作,我们只需要在某一个地方插入dispatch_barrier_async,如下面所示:

····
dispatch_async(queue,doread2);
dispatch_barrier_async(queue,dowrite);
dispatch_async(queue,doread3);
····

当我们的代码执行到dispatch_barrier_async时,它会先等待已经在queue中处理的任务处理完毕,再把自己的任务放入queue,同时将剩下的任务给阻拦起来(barrier),相当于一个串行队列阻塞的作用。当自己的任务执行完毕,我们的queue才又恢复正常的工作,这也是一种处理多线程并发问题的解决方案。

NSOperation

NSOperation是苹果公司对于GCD的封装,虽然理念和GCD一样,都是通过队列+操作的形式,但是对程序员友好多了。

在NSOperation中,任务需要被封装成一个对象才能被执行。可以封装成NSInvocationOperation或者NSBlockOperation,这俩货都是NSOperation的子类。你可以将方法名传入,就像这样

    NSInvocationOperation *operation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(onTest) object:nil];
    [operation start];

你也可以这样,把一个block传入

  NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
      NSLog(@"6666");
  }];
  [operation start];

但是推荐使用NSBlockOperation,因为NSInvocationOperation不是类型安全,在swift中已经去掉这个类了,OC感觉也快了。很明显可以看出,start是开始执行任务的标志。当我们调用start方法的时候,会默认在当前线程同步执行,这就相当于我们上文GCD的同步操作串行/并行队列。

那我需要异步操作并行队列怎么办呢?在NSBlockOperation中还有一个addExecutionBlock方法,调用这个方法就可以达到我们的目标。

    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
    }];
    
    for (NSInteger i = 0; i < 5; i++) {
        [operation addExecutionBlock:^{
            NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
        }];
    }
    [operation start];

打出的LOG和我们异步调用并行队列一模一样

2016-09-28 15:17:06.764 testGCD[7812:2042404] {number = 1, name = main}
2016-09-28 15:17:06.766 testGCD[7812:2042450] 第1次:{number = 3, name = (null)}
2016-09-28 15:17:06.766 testGCD[7812:2042458] 第2次:{number = 4, name = (null)}
2016-09-28 15:17:06.768 testGCD[7812:2042404] 第3次:{number = 1, name = main}
2016-09-28 15:17:06.765 testGCD[7812:2042437] 第0次:{number = 2, name = (null)}
2016-09-28 15:17:06.770 testGCD[7812:2042450] 第4次:{number = 3, name = (null)}

但是不推荐用这个方法,有替代的方法下面说。

NSOperationQueue

上面的例子其实还差一个异步调用串行队列,但是在讲这个用法之前,先把NSOperationQueue讲了,因为我们上面都是把任务封装了一下,看起来没队列什么事就执行了,事实上那些任务都是被包裹在队列中被执行的,因此在讲异步调用串行队列的使用之前,先把队列这里给讲了。

同GCD一样,NSOperationQueue也分为两种队列,串行和并行,而串行队列中最典型的就是main队列,如下所示:

NSOperationQueue *queue = [NSOperationQueue mainQueue];

剩下的其他队列:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
···
[queue addOperation:operation];

这样就把任务添加到了队列之中,另外再补充一点,所有添加到队列的任务都会自动调用start方法。

下面就是重点了(敲黑板),NSOperation对GCD做了封装,把难理解的同步异步线程给整合成一个参数maxConcurrentOperationCount。这个参数能干什么呢,它能控制Operation的最大并发数,通过控制最大并发数,我们就能控制线程数。因为同时只能有一个Operation执行的话,系统只会给我们开一个线程,这样就相当于GCD的异步串行操作。就像这样

queue.maxConcurrentOperationCount=1;

OK,我觉得要是较真的人现在已经开始把这段代码加入之前的并行异步操作的代码进行尝试了,但是发现不起作用,也就是最大并发数失效。这就是上面不推荐用那段代码并行操作的原因。maxConcurrentOperationCount,字面翻译都是最大的ConcurrentOperation数量,而我们看看上面的代码,明明只有一个Operation,只不过我们在里面给Operation封了很多任务而已。maxConcurrentOperationCount只能控制Operation的数量,并不能控制Operation中任务的并发数量,所以导致了maxConcurrentOperationCount失效。OK,那正常的使用方法是怎样,如下所示:

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//  queue.maxConcurrentOperationCount=1;  
    for (NSInteger i = 0; i < 5; i++) {
        [queue addOperationWithBlock:^{
        NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
        }];
    }

我们直接通过NSOperationQueue中的addOperationWithBlock把block封入一个Operation,最后把这一堆Operation放入queue中,而不是像之前那样把一堆block封入一个Operation,再把Operation放入queue,这样最大并发数就无法控制了。

添加依赖

GCD中添加block的执行先后比较繁琐,而在NSOperation中则简单多了。它把执行的先后做成一个API接口,通过API接口我们可以轻松的设置block执行顺序。这个接口就是addDependency,添加依赖。用法如下:

    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"A - %@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:1.0];
    }];

    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"B   - %@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:1.0];
    }];

    NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"C - %@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:1.0];
    }];
    
    //B任务添加依赖A
    [operation2 addDependency:operation1];
    //C任务添加依赖B
    [operation3 addDependency:operation2];
    //所以执行顺序就是ABC

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];

特别注意,千万别相互依赖,会死锁!!!!

其他用法

  • NSOperation
BOOL executing; //判断任务是否正在执行

BOOL finished; //判断任务是否完成

void (^completionBlock)(void); //用来设置完成后需要执行的操作

- (void)cancel; //取消任务

- (void)waitUntilFinished; //阻塞当前线程直到此任务执行完毕

  • NSOperationQueue
NSUInteger operationCount; //获取队列的任务数

- (void)cancelAllOperations; //取消队列中所有的任务

- (void)waitUntilAllOperationsAreFinished; //阻塞当前线程直到此队列中的所有任务执行完毕

[queue setSuspended:YES]; // 暂停queue

[queue setSuspended:NO]; // 继续queue

优劣比较

多线程编程技术的优缺点比较

  • NSThread (抽象层次:低)

    • 优点:轻量级,简单易用,可以直接操作线程对象
    • 缺点: 需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销。
  • Cocoa NSOperation (抽象层次:中)

    • 优点:不需要关心线程管理,数据同步的事情,可以把精力放在学要执行的操作上。基于GCD,是对GCD 的封装,比GCD更加面向对象
    • 缺点: NSOperation是个抽象类,使用它必须使用它的子类,可以实现它或者使用它定义好的两个子类NSInvocationOperation、NSBlockOperation.
  • GCD 全称Grand Center Dispatch (抽象层次:高)

    • 优点:是 Apple 开发的一个多核编程的解决方法,简单易用,效率高,速度快,基于C语言,更底层更高效,并且不是Cocoa框架的一部分,自动管理线程生命周期(创建线程、调度任务、销毁线程)。
    • 缺点: 使用GCD的场景如果很复杂,就有非常大的可能遇到死锁问题。

常见并发问题及其解决方案

死锁

死锁在多线程编程中一不留神就出现了,例如下面这段代码就是典型的死锁:

    dispatch_sync(dispatch_get_main_queue(), ^{
        //造成死锁了
        
    });

主线程等待主线程执行完毕再执行block,自己等待自己结果造成了死循环,如果遇到LOG打不出来的情况,第一个就要检查queue是否循环等待了。

互斥

这个也是常见的,一件事不能俩线程同时干,同时就会出问题。一般这样情况就可以使用@synchronized给代码加上互斥锁。

@synchronized(self){


}

同步执行

拿GCD来说,如果是在一个类里还比较好同步,但是项目大了以后,有可能很多地方都需要同步,这时候通常的做法就是设置一个全局queue来处理同步问题:

  dispatch_sync(queue, ^{
      NSInteger times = lastTimes;
      [NSThread sleepForTimeInterval:1];
      NSLog(@"调用次数:%ld ,调用线程: %@",times, [NSThread currentThread]);
      times -= 1;
      lastTimes = times;
  });

你可能感兴趣的:(IOS多线程总结)