iOS多线程

OC中的多线程

OC中多线程根据封装程度可以分为三个:NSThread、GCD和NSOperation,本文主要讲的是GCD的使用。

一: NSThread

NSThread是封装程度最小最轻量级的,使用更灵活,但要手动管理线程的生命周期、线程同步和线程加锁等,开销较大;因为需要自己手动控制线程的生命周期,比较麻烦,因此用的比较少。

1.1、 简单使用
 //创建线程
   NSThread *newThread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:@"Thread"];
   //或者
   NSThread  *newThread=[[NSThread alloc] init];
   NSThread  *newThread= [[NSThread alloc]initWithBlock:^{
       NSLog(@"initWithBlock");
   }];
//设置优先级
newThread.qualityOfService = NSQualityOfServiceUserInteractive;
//启动线程
[newThread start];
//取消线程
[newThread cancel];
1.2、实用方法

尽管NSThread大部分功能用起来比较麻烦,但是有一些小方法还是比较方便使用的,也是我会在项目中用到的

//获取当前线程
[NSThread currentThread];
//获取主线程
[NSThread mainThread];
//睡眠当前线程 5秒
 [NSThread sleepForTimeInterval:5];
//5秒后执行run方法
[self performSelector:@selector(run) withObject:nil afterDelay:5.0];
//回到主线程执行run方法
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES];

二:NSOperation

NSOperation是基于GCD的一个抽象基类,是GCD的封装,属于object-c类。将线程封装成要执行的操作,不需要管理线程的生命周期和同步,比GCD可控性更强,例如可以加入操作依赖(addDependency)、设置操作队列最大可并发执行的操作个数(setMaxConcurrentOperationCount)、取消操作(cancel)等。

NSOperation 是个抽象类,不能用来封装操作。我们只有使用它的子类来封装操作。我们有三种方式来封装操作,分别是使用子类 NSInvocationOperation、NSBlockOperation或者自定义继承自 NSOperation 的子类,通过实现内部相应的方法来封装操作

2.1、子类NSInvocationOperation使用
// 1.创建 NSInvocationOperation 对象
   NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];
   // 2.调用 start 方法开始执行操作
   [op start];
//执行的方法
- (void)task {
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}

当前线程同步执行,直接 [self task]不香吗

2.2子类NSBlockOperation使用
//创建 NSBlockOperation 对象
 NSBlockOperation *blkOperation = [NSBlockOperation blockOperationWithBlock:^{
     NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}];
[blkOperation start];

当前线程同步执行,但是NSBlockOperation可以添加并发。

//添加额外的操作
[op addExecutionBlock:^{
     [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
     NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}];

当并发数量大于1时,就会自动开启新线程,多写一个addExecutionBlock即多添加一个并发。另:查看网上并没有说这种并发的上限,但经过我的测试,并发上限跟当前机器的核数有关,即6核最多同时开启6个并发,8核8个。

2.3自定义继承自 NSOperation 的子类使用(略)
2.4NSOperation最强大用法--创建队列(NSOperationQueue)的使用

NSOperationQueue 一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。下边是主队列、自定义队列的基本创建方法和特点。

  • 主队列
    • 凡是添加到主队列中的操作,都会放到主线程中执行,也就不存在并发、异步等了
    • 可以用它去实现回归主线程操作
// 主队列获取方法
NSOperationQueue *queue = [NSOperationQueue mainQueue];
  • 自定义队列
    • 添加到这种队列中的操作,就会自动放到子线程中执行
    • 同时包含了:串行、并发功能。
// 自定义队列创建方法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
将操作加入到队列中

上边我们说到 NSOperation 需要配合 NSOperationQueue 来实现多线程。那么我们需要将创建好的操作加入到队列中去。总共有两种方法:

  • (void)addOperation:(NSOperation *)op
    需要先创建操作,再将创建好的操作加入到创建好的队列中去。
 // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    //2.设置最大并发操作数
    queue.maxConcurrentOperationCount = 5;
    
    // 3.创建操作
    // 使用 NSInvocationOperation 创建操作1
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
    
    // 使用 NSInvocationOperation 创建操作2
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];

// 4.使用 addOperation: 添加所有操作到队列中
    [queue addOperation:op1]; 
    [queue addOperation:op2]; 

此时就会开启异线程并发执行,最大并发数为maxConcurrentOperationCount设置的值,maxConcurrentOperationCount最大值为64,需要注意的是多条线程是高效,但也并不是越多越好,线程多了会更耗CPU,所以我们需要控制线程最大并发数。

  • (void)addOperationWithBlock:(void (^)(void))block;
    无需先创建操作,在 block 中添加操作,直接将包含操作的 block 加入到队列中
// 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
  
    //2.设置最大并发操作数
    queue.maxConcurrentOperationCount = 2;
    
    // 3.使用 addOperationWithBlock: 添加操作到队列中
    [queue addOperationWithBlock:^{
         [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
         NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }];
    [queue addOperationWithBlock:^{
         [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
         NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    }];

三:GCD

  • GCD 是基于 C 的 API
  • GCD 可用于多核的并行运算
  • GCD 会自动利用更多的 CPU 内核
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

这是官方对其介绍


GCD

翻译:Dispatch,也称为Grand Central Dispatch(GCD),包含语言功能、运行时库和系统增强功能,这些功能为支持macOS、iOS、watchOS和tvOS中的多核硬件上的并发代码执行提供了系统的、全面的改进。
官方文档地址

3.1、GCD 任务

任务就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步任务和异步任务。两者的主要区别是:是否需要等待队列的任务执行结束,以及是否具备开启新线程的能力

  • 同步任务(sync)
    • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
    • 只能在当前线程中执行任务,不具备开启新线程的能力。
  • 异步任务(async)
    • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
    • 可以在新的线程中执行任务,具备开启新线程的能力( 但是并不一定开启新线程。这跟任务所指定的队列类型有关)。
3.2、GCD 队列

队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。在 GCD 中有两种队列:串行队列和并发队列,两者的主要区别是:执行顺序不同,以及开启线程数不同

  • 串行队列
    • 每次只有一个任务被执行。让任务一个接着一个地执行。一个任务执行完毕后,再执行下一个任务。
    • 只开启一个新线程(或者不开启新线程,在当前线程执行任务)。
  • 并发队列
    • 可以让多个任务并发(同时)执行。
    • 可以开启多个线程,并且同时执行任务。

注意:并发队列的并发功能只有在异步(dispatch_async)函数下才有效

串行队列.png
并发队列.png
3.3、GCD 的使用

GCD 的使用步骤其实很简单,只有两步:

  • 创建一个队列(串行队列或并发队列);
  • 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步任务或异步任务)。
3.4、队列的创建

可以用dispatch_queue_create来创建,包含两个参数

  • 第一个参数是标识:

    • 指定生成返回的Dispatch Queue的名称
    • 命名规则:简单易懂
    • 该名称在Xcode和Instruments的调试器中作为Dispatch Queue的名称表示
    • 该名称也会出现在程序崩溃时所生成的CrashLog中
  • 第二个参数是队列类型:

    • DISPATCH_QUEUE_SERIAL 表示串行队列
    • DISPATCH_QUEUE_CONCURRENT 表示并发队列
    • NULL 则会默认认为是DISPATCH_QUEUE_SERIAL
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("identification", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("identification", DISPATCH_QUEUE_CONCURRENT);

其中串行队列又有一个队列叫主队列

  • 所有放在主队列中的任务,都会放到主线程中执行
// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();

并发队列又有一个队列叫全局并发队列

  • 使用dispatch_get_global_queue来获取,需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT;第二个参数暂时没用,用0即可
// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
3.5、任务的创建

GCD的任务分两种,同步任务和异步任务分别用dispatch_sync和dispatch_async创建

// 同步执行任务创建方法
dispatch_sync(queue, ^{
    // 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
    // 这里放异步执行任务代码
});

根据队列和任务的不同,我们可以将GCD的使用分为下面几种组合

1.同步任务 + 串行队列
2.异步任务 + 串行队列
3.同步任务 + 并发队列
4.异步任务 + 并发队列
上面我们又说了串行队列和并发队列各有一种特殊用法,分别是主队列和全局并发队列;其中全局并发队列和并发队列用法基本一致,将放在一起讨论了;主队列则很有必要专门来研究一下,所以我们就多了两种组合方式:
5.同步任务 + 主队列
6.异步任务 + 主队列

3.6、 任务和队列不同组合方式的区别

我们先来考虑最基本的使用,不同队列+不同任务 简单组合使用的不同区别。暂时不考虑队列中嵌套队列的这种复杂情况。(注:因为主队列+同步在主线程和其他线程中结果差异很大,会简单区分讨论下,其他暂时默认都是在主线程的环境下调用)

不同队列+不同任务组合的区别

区别 串行队列 并发队列 主队列
同步(sync) 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 主线程:死锁卡住
其他线程:没有开启新
线程,串行执行任务
异步(async) 有开启新线程(1条),串行执行任务 有开启新线程,并发执行任务 没有开启新线程,串行执行任务
a、同步任务 + 串行队列
  • 不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"同步串行---begin");
    
    dispatch_queue_t queue = dispatch_queue_create("sky.testQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(queue, ^{
        // 追加任务 1
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_sync(queue, ^{
        // 追加任务 2
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_sync(queue, ^{
        // 追加任务 3
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"同步串行---end");

输出结果:
2020-07-10 14:18:45.873274+0800 TestOC[11204:603832] currentThread---{number = 1, name = main}

2020-07-10 14:18:45.873401+0800 TestOC[11204:603832] 同步串行---begin
2020-07-10 14:18:47.873699+0800 TestOC[11204:603832] 1---{number = 1, name = main}
2020-07-10 14:18:49.874353+0800 TestOC[11204:603832] 2---{number = 1, name = main}
2020-07-10 14:18:51.875848+0800 TestOC[11204:603832] 3---{number = 1, name = main}
2020-07-10 14:18:51.876233+0800 TestOC[11204:603832] 同步串行---end

小结:

我们可以发现:

  • 所有代码都在主线程中执行,即当前线程中执行,并没有开启新的线程
  • 所有的任务都是在同步串行---begin同步串行---end之间执行,即需要等待队列执行结束后才会执行后面的代码
  • 任务是按顺序执行的 ,即每次只有一个任务被执行,任务一个接一个按顺序执行,遵循FIFO原则
b、异步任务 + 串行队列
  • 会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"异步串行---begin");
    
    dispatch_queue_t queue = dispatch_queue_create("sky.testQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{
        // 追加任务 1
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 2
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 3
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"异步串行---end");

输出结果:
2020-07-10 14:28:08.908495+0800 TestOC[11272:609791] currentThread---{number = 1, name = main}
2020-07-10 14:28:08.908612+0800 TestOC[11272:609791] 异步串行---begin
2020-07-10 14:28:08.908720+0800 TestOC[11272:609791] 异步串行---end
2020-07-10 14:28:10.913096+0800 TestOC[11272:609937] 1---{number = 6, name = (null)}
2020-07-10 14:28:12.917224+0800 TestOC[11272:609937] 2---{number = 6, name = (null)}
2020-07-10 14:28:14.918440+0800 TestOC[11272:609937] 3---{number = 6, name = (null)}

小结:

我们可以发现:

  • 开启了新线程,但是只开启了一条(异步任务具备开启新线程的能力,但串行队列限制其只能开启一个线程)
  • 所有任务是在打印的 异步串行---begin异步串行---end 之后才开始执行的,,即异步执行不会等待队列执行结束,会直接先执行后面的代码
  • 任务是按顺序执行的 ,即每次只有一个任务被执行,任务一个接一个按顺序执行,遵循FIFO原则
c、同步任务 + 并发队列
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"同步并发---begin");
    
    dispatch_queue_t queue = dispatch_queue_create("sky.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_sync(queue, ^{
        // 追加任务 1
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_sync(queue, ^{
        // 追加任务 2
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_sync(queue, ^{
        // 追加任务 3
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"同步并发---end");

输出结果:
2020-07-10 14:35:32.679831+0800 TestOC[11350:614865] currentThread---{number = 1, name = main}
2020-07-10 14:35:32.679967+0800 TestOC[11350:614865] 同步并发---begin
2020-07-10 14:35:34.680461+0800 TestOC[11350:614865] 1---{number = 1, name = main}
2020-07-10 14:35:36.681217+0800 TestOC[11350:614865] 2---{number = 1, name = main}
2020-07-10 14:35:38.682705+0800 TestOC[11350:614865] 3---{number = 1, name = main}

2020-07-10 14:35:38.682950+0800 TestOC[11350:614865] 同步并发---end

小结:

我们可以发现:

  • 所有代码都在主线程中执行,即当前线程中执行,并没有开启新的线程
  • 所有的任务都是在同步串行---begin同步串行---end之间执行,即需要等待队列执行结束后才会执行后面的代码
  • 任务是按顺序执行的 ,即每次只有一个任务被执行,任务一个接一个按顺序执行,遵循FIFO原则
  • 尽管并发队列可以开启多个线程,并且同时执行多个任务,但是同步任务不具备开启新线程的能力,因此只能在当前线程一个个执行。就像你去公司有很大方法(打车、开车、走路等),当时你现在身上啥也没有,你就只能走路去了
d、异步任务 + 并发队列
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"异步并发---begin");
    
    dispatch_queue_t queue = dispatch_queue_create("sky.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        // 追加任务 1
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 2
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 3
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"异步并发---end");

输出结果:
2020-07-10 14:57:08.983092+0800 TestOC[11438:626363] currentThread---{number = 1, name = main}
2020-07-10 14:57:08.983220+0800 TestOC[11438:626363] 异步并发---begin
2020-07-10 14:57:08.983308+0800 TestOC[11438:626363] 异步并发---end
2020-07-10 14:57:10.986914+0800 TestOC[11438:626468] 3---{number = 7, name = (null)}
2020-07-10 14:57:10.986917+0800 TestOC[11438:626473] 1---{number = 3, name = (null)}
2020-07-10 14:57:10.986984+0800 TestOC[11438:626493] 2---{number = 6, name = (null)}

小结:

我们可以发现:

  • 除了当前线程,系统又开启了新的线程,并且任务是同时执行的
  • 所有任务是在打印的 异步并发---begin异步并发---end 之后才开始执行的,,即异步执行不会等待队列执行结束,会直接先执行后面的代码
e、同步任务 + 主队列
在主线程中执行
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"同步主队列---begin");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_sync(queue, ^{
        // 追加任务 1
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_sync(queue, ^{
        // 追加任务 2
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_sync(queue, ^{
        // 追加任务 3
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"同步主队列---end");

输出结果:
2020-07-10 15:52:05.332135+0800 TestOC[11586:652198] currentThread---{number = 1, name = main}
2020-07-10 15:52:05.332254+0800 TestOC[11586:652198] 同步主队列---begin
(lldb)
卡在第一个dispatch_sync(queue, ^{ 地方报错

主队列.png
小结:

我们可以发现:

  • 追加任务 1、2、3都没有实现,同步主队列---end也没有打印,并且直接报错
  • 这是因为我们把任务放到了主线程队列中,而同步任务会等待当前队列中的任务执行完毕,才会接着执行,即主线程队列中的任务执行完毕;但是此时我们把任务1添加到了主线程队列中,就会造成任务1等待主线程队列执行完毕后才会执行,主线程队列要等待任务1执行完毕后才会执行完毕,从而造成相互等待,形成死锁
在其他线程中调用中执行
// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行 selector 任务
    [NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
-(void)syncMain {
    
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"同步主队列---begin");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_sync(queue, ^{
        // 追加任务 1
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_sync(queue, ^{
        // 追加任务 2
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_sync(queue, ^{
        // 追加任务 3
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"同步主队列---end");
    
}

输出结果:
2020-07-10 16:34:17.706859+0800 TestOC[11706:672887] currentThread---{number = 6, name = (null)}
2020-07-10 16:34:17.707026+0800 TestOC[11706:672887] 同步主队列---begin
2020-07-10 16:34:19.718782+0800 TestOC[11706:672720] 1---{number = 1, name = main}
2020-07-10 16:34:21.719807+0800 TestOC[11706:672720] 2---{number = 1, name = main}
2020-07-10 16:34:23.721612+0800 TestOC[11706:672720] 3---{number = 1, name = main}
2020-07-10 16:34:23.722060+0800 TestOC[11706:672887] 同步主队列---end

小结:

我们可以发现:

  • 所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程,因为所有放在主队列的任务都会在主线程中执行
  • 所有的任务都是在同步主队列---begin同步主队列---end之间执行,即需要等待队列执行结束后才会执行后面的代码
  • 任务是按顺序执行的 ,即每次只有一个任务被执行,任务一个接一个按顺序执行,遵循FIFO原则

为什么在其他线程中不会死锁呢?
因为当前队列在当前线程中,而任务 1、任务 2、任务3 都在追加到主队列时,会自动放在主线程的队列中,因此当前队列中不存在其他任务,可以执行完毕,然后去执行任务1,然后任务2、任务3。所以这里不会卡住线程,也就不会造成死锁问题。

f、异步任务 + 主队列
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"异步主队列---begin");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_async(queue, ^{
        // 追加任务 1
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 2
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 3
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"异步主队列---end");

输出结果:
2020-07-10 16:50:43.285808+0800 TestOC[11891:683408] currentThread---{number = 1, name = main}
2020-07-10 16:50:43.285926+0800 TestOC[11891:683408] 异步主队列---begin
2020-07-10 16:50:43.286015+0800 TestOC[11891:683408] 异步主队列---end
2020-07-10 16:50:45.296442+0800 TestOC[11891:683408] 1---{number = 1, name = main}
2020-07-10 16:50:47.297820+0800 TestOC[11891:683408] 2---{number = 1, name = main}
2020-07-10 16:50:49.299448+0800 TestOC[11891:683408] 3---{number = 1, name = main}

小结:

我们可以发现:

  • 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然 异步执行 具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
  • 所有任务是在打印的 异步主队列---begin 和 异步主队列---end 之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
  • 任务是按顺序执行的 ,即每次只有一个任务被执行,任务一个接一个按顺序执行,遵循FIFO原则
  • 有人会问,这个跟主线程中执行 同步任务 + 主队列 一样,所有的都是在主线程队列里面执行,为什么没有死锁,这是因为异步执行不会做任何等待,不需要等待主线程当前队列结束,直接可以去执行任务1,也就不存在相互等待死锁了
3.7、GCD 嵌套队列

嵌套队列也就是在队列中镶嵌队列,也是线程间的通信。
线程间的通信主要就是队列中嵌套队列。感觉上面3.6里面同步任务 + 并发队列、同步任务 + 串行队列 并没有多少用到的地方,且意义不大,就不多做介绍了。

嵌套队列之队列里面嵌套主队列

在 iOS 开发过程中,我们一般需要在主线程里边进行 UI 刷新。而我们又会把一些耗时操作放在其他线程里面,如:图片下载、文件上传等耗时操作。而当我们在其他线程完成了耗时操作时,需要回到主线程UI 刷新,那么就用到了线程之间的通讯。场景模拟,如:在一个UITableView列表上点击上传实现文件上传,文件上传后刷新列表将上传按钮改成已上传等操作。

    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"---begin");
    
    // 获取并发队列
    dispatch_queue_t queue = dispatch_queue_create("sky.testQueue", DISPATCH_QUEUE_CONCURRENT);
    // 获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_async(queue, ^{
        // 异步追加任务 1
        sleep(2);                                       // 模拟耗时操作,如:文件上传
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        
        // 回到主线程
        dispatch_async(mainQueue, ^{
            // 追加在主线程中执行的任务
            sleep(1);                                       // UI刷新
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
    });
    
    dispatch_async(queue, ^{
        // 异步追加任务 2
        sleep(4);                                       // 模拟耗时操作,如:文件上传
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        
        // 回到主线程
        dispatch_async(mainQueue, ^{
            // 追加在主线程中执行的任务
            sleep(2);                                       // UI刷新
            NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
        });
    });

    NSLog(@"---end");

输出结果:
2020-07-13 13:36:54.413184+0800 TestOC[14492:913025] currentThread---{number = 1, name = main}
2020-07-13 13:36:54.413276+0800 TestOC[14492:913025] ---begin
2020-07-13 13:36:54.413394+0800 TestOC[14492:913025] ---end
2020-07-13 13:36:56.418424+0800 TestOC[14492:913135] 1---{number = 5, name = (null)}
2020-07-13 13:36:57.419698+0800 TestOC[14492:913025] 2---{number = 1, name = main}
2020-07-13 13:36:58.414204+0800 TestOC[14492:913131] 3---{number = 6, name = (null)}
2020-07-13 13:37:00.415765+0800 TestOC[14492:913025] 4---{number = 1, name = main}

小结:

我们可以发现:

  • 模拟下载的耗时任务是在其他线程实现的,并且不需要主线程等待(---end在耗时任务之前就打印了)

  • 模拟刷新的任务在完成模拟耗时任务后回到主线程之后在实现的,因此我们可以实现在其他线程完成耗时任务后回到主线程实现UI刷新任务的需求

  • 注意:

  • 上面的说具体点是异步并发队列里面嵌套异步主队列,也是用到最多的一种嵌套主队列

  • 同步并发队列里面嵌套异步主队列,不会开启新线程,在当前线程里按顺序执行模拟的耗时任务,然后再回到主线程执行嵌套的主队列的UI刷新任务;即执行完所有耗时任务后再去主线程执行刷新任务

  • 异步串行队列里面嵌套异步主队列,会开启一条新线程,按顺序先在新线程里面按执行模拟的耗时任务,然后再回到主线程执行嵌套的主队列的UI刷新任务,在接着执行下一个耗时任务;即每完成一个耗时任务,会先执行刷新任务,然后再去执行下一个耗时任务

  • 同步串行队列里面嵌套异步主队列,结果和同步并发队列里面嵌套异步主队列一样

  • 队列里面嵌套同步主队列,如果外层队列是在主线程,则会造成死锁,所以为了避免不必要bug,不推荐使用同步主队列。

嵌套队列之其他嵌套

死锁嵌套:串行队列嵌套串行队列

    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"---begin");
    
    dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{    // 异步执行 + 串行队列
        sleep(2);                                       // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        dispatch_sync(queue, ^{  // 同步执行 + 当前串行队列
            // 追加任务 1
            sleep(2);                                       // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
    });
    NSLog(@"---end");

输出结果:
2020-07-13 15:54:24.064504+0800 TestOC[15548:987245] currentThread---{number = 1, name = main}
2020-07-13 15:54:24.064707+0800 TestOC[15548:987245] ---begin
2020-07-13 15:54:24.064875+0800 TestOC[15548:987245] ---end
2020-07-13 15:54:26.068574+0800 TestOC[15548:987599] 1---{number = 7, name = (null)}
(lldb)

小结:

我们可以发现:

  • 在追加任务 1的时候,程序会崩溃
  • 这是因为串行队列中追加的任务 和 串行队列中原有的任务 两者之间相互等待,阻塞了串行队列,最终造成了串行队列所在的线程死锁问题
  • 这个原因和主队列造成死锁的原因基本一致,都是相互等待

嵌套队列的种类实在是太多了,而且在我代码生涯中,除了使用异步并发队列里面嵌套异步主队列这种外,剩下的都有用到,就不具体分析了,就在下面简单分析下同一个队列互相嵌套问题,如果对多队列互相嵌套或者多层嵌套有兴趣,可以自己去实验(话说根本用不到)
同一个队列互相嵌套组合的区别

区别 异步并发嵌套同一个并发队列 同步并发嵌套同一个并发队列 异步串行嵌套同一个串行队列 同步串行嵌套同一个串行队列
同步(sync) 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 死锁卡住不执行 死锁卡住不执行
异步(async) 有开启新线程,并发执行任务 有开启新线程,并发执行任务 有开启新线程(1条),串行执行任务 有开启新线程(1条),串行执行任务
3.8、 GCD 的其他方法
3.8.1、GCD 栅栏方法:dispatch_barrier_async

有时候我们需要异步执行多组操作,比喻我以前的一个地铁类项目,需要先根据线路获取站点,在根据站点去获取对应数据,这样我们就一个将根据线路获取站点的操作分成一个组,等待这组所有请求都完成后再去执行根据站点去获取对应数据的请求。此时,我们将需要一个相当如栅栏的方法,将两组异步操作分割起来,执行完第一组的全部操作后再去执行第二组,当然,一个操作组里面可以包含多个任务,这就需要用到dispatch_barrier_async 方法在两个操作组间形成栅栏。
dispatch_barrier_async 方法会等待前边追加到并发队列中的任务全部执行完毕之后,再将 dispatch_barrier_async 方法中追加的任务执行完毕之后,然后再追加任务到该异步队列并开始执行。先执行第一组任务,然后执行栅里面的任务,最后执行第二组任务,具体如下图所示:


栅栏.png
   //    栅栏方法 dispatch_barrier_async
   dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
   dispatch_async(queue, ^{
       // 追加任务 1
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
   });
   dispatch_async(queue, ^{
       // 追加任务 2
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
   });
   
   dispatch_barrier_async(queue, ^{
       // 追加任务 barrier1
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"barrier1---%@",[NSThread currentThread]);// 打印当前线程
   });
   
   dispatch_async(queue, ^{
       // 追加任务 3
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
   });
   dispatch_async(queue, ^{
       // 追加任务 4
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
   });

输出结果:
2020-07-13 17:05:20.408922+0800 TestOC[15836:1028107] 2---{number = 3, name = (null)}
2020-07-13 17:05:20.408935+0800 TestOC[15836:1028147] 1---{number = 5, name = (null)}
2020-07-13 17:05:22.413781+0800 TestOC[15836:1028147] barrier1---{number = 5, name = (null)}
2020-07-13 17:05:24.417003+0800 TestOC[15836:1028147] 3---{number = 5, name = (null)}
2020-07-13 17:05:24.417000+0800 TestOC[15836:1028107] 4---{number = 3, name = (null)}

小结:

我们可以发现:

  • 在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作
  • 如果是多组,执行顺序是:第一组-->栏栅1-->第二组-->栏栅2-->...
3.8.2、GCD 延时执行方法:dispatch_after

在开发中,我们有时会遇见这样的需求,在2秒后执行某个任务,这是就可以用GCD的dispatch_after方法来实现

NSLog(@"---begin");
   //    延时执行方法 dispatch_after
   //    NSEC_PER_SEC表示秒,可以根据需求换成毫秒等
   dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
       // 2.0 秒后异步追加任务代码到主队列,并开始执行
       NSLog(@"---after");  // 打印当前线程
   });
   NSLog(@"---end");

输出结果:
2020-07-13 17:14:40.454448+0800 TestOC[16025:1035485] ---begin
2020-07-13 17:14:40.454550+0800 TestOC[16025:1035485] ---end
2020-07-13 17:14:42.454623+0800 TestOC[16025:1035485] ---after

小结:

我们可以发现:

  • dispatch_after 方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中,严格来说,这个时间并不是绝对准确的。
  • dispatch_after 默认是回到主线程去执行延时任务的
  • dispatch_after不会让延时任务,会直接先执行后面的代码
3.8.3、 GCD 一次性代码(只执行一次):dispatch_once

有时为了优化程序,有的代码在整个程序运行过程中只需执行一次时,我们就用到了 GCD 的 dispatch_once 方法。使用 dispatch_once 方法能保证某段代码在程序运行过程中只被执行 1 次,并且即使在多线程的环境下,dispatch_once 也可以保证线程安全。最常用的地方就是创建单例。如:现在需要创建一个单例AppConfig

static AppConfig *appConfig = nil;

//单例
+ (AppConfig *)shareInstance {
   
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
       //不管调用shareInstance多少次,初始化appConfig只会执行一次
       appConfig = [[AppConfig alloc] init];
   });
   return appConfig;
}
3.8.4、 GCD 快速迭代方法:dispatch_apply

通常我们会用 for 循环遍历,但是如果里面操作比较耗时,那么加起来等待的时间就会比较久,此时就可以使用GCD 给我们提供了快速迭代的方法 dispatch_apply。dispatch_apply 按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束后在继续执行后续任务。
如果在串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行,这样就体现不出快速迭代的意义了。
但是在并发队列中使用 dispatch_apply,那么就会并发去处理耗时操作, 这样执行起来就会比for 循环节省时间了。
还有一点,无论是在串行队列,还是并发队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法。

NSLog(@"---begin");
   //1.创建NSArray类对象
   NSArray *array = @[@"a", @"b", @"c", @"d", @"e", @"f", @"g", @"h", @"i", @"j"];
   //2.创建一个全局队列
   dispatch_queue_t queue = dispatch_queue_create("sky.testQueue", DISPATCH_QUEUE_CONCURRENT);
   //3.通过dispatch_apply函数对NSArray中的全部元素进行处理,并等待处理完成,
   dispatch_apply([array count], queue, ^(size_t index) {
       sleep(2);
       NSLog(@"%zd: %@", index, [array objectAtIndex:index]);
   });
   NSLog(@"---end");

输出结果:
2020-07-13 17:44:18.256883+0800 TestOC[1323:216847] ---begin
2020-07-13 17:44:20.258034+0800 TestOC[1323:217073] 0: a
2020-07-13 17:44:20.258037+0800 TestOC[1323:217067] 1: b
2020-07-13 17:44:20.258089+0800 TestOC[1323:216847] 2: c
2020-07-13 17:44:20.258094+0800 TestOC[1323:217095] 5: f
2020-07-13 17:44:20.258228+0800 TestOC[1323:217094] 3: d
2020-07-13 17:44:20.258232+0800 TestOC[1323:217093] 4: e
2020-07-13 17:44:22.259244+0800 TestOC[1323:217067] 6: g
2020-07-13 17:44:22.259244+0800 TestOC[1323:216847] 9: j
2020-07-13 17:44:22.259256+0800 TestOC[1323:217095] 8: i
2020-07-13 17:44:22.259244+0800 TestOC[1323:217073] 7: h
2020-07-13 17:44:22.259667+0800 TestOC[1323:216847] ---end

小结:

我们可以发现:

  • 并发执行,可以同时执行多个任务,但是需要注意的是同addExecutionBlock一样, 并发上限跟当前机器的核数有关,即6核最多同时开启6个并发,8核8个。(我测试的机型是iPhone8是,一次最多6个并发,当我换成iPhone11 Pro Max时,一次最多并发就变成了8个)
  • ---end在最后才打印,说明需要等待dispatch_apply全部执行完毕后,才会继续后续操作
3.8.5、GCD 队列组:dispatch_group

a、dispatch_group_wait

有时我们会遇见这样的需求,登陆的时候获取从多了接口基础数据,当所有的接口数据都获取完后,再跳转到登陆界面,这时候我们可以用到 GCD 的队列组。

  • 调用队列组的 dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enter、dispatch_group_leave 组合来实现 dispatch_group_async
  • 调用队列组的 dispatch_group_notify 回到指定线程执行任务
   NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
   NSLog(@"group---begin");
   
   dispatch_group_t group =  dispatch_group_create();
   
   dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       // 追加任务 1
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
   });
   
   dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       // 追加任务 2
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
   });
   
   dispatch_group_notify(group, dispatch_get_main_queue(), ^{
       // 等前面的异步任务 1、任务 2 都执行完毕后,回到主线程执行下边任务
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
       
       NSLog(@"group---end");
   });
   NSLog(@"group---end");

输出结果:
2020-07-13 19:52:11.985055+0800 TestOC[17048:1121766] currentThread---{number = 1, name = main}
2020-07-13 19:52:11.985256+0800 TestOC[17048:1121766] group---begin
2020-07-13 19:52:11.985446+0800 TestOC[17048:1121766] group---end
2020-07-13 19:52:13.986024+0800 TestOC[17048:1122478] 1---{number = 6, name = (null)}
2020-07-13 19:52:13.986023+0800 TestOC[17048:1122378] 2---{number = 4, name = (null)}
2020-07-13 19:52:15.986499+0800 TestOC[17048:1121766] 3---{number = 1, name = main}
2020-07-13 19:52:15.986838+0800 TestOC[17048:1121766] group---end

小结:

我们可以发现:

  • 异步操作都在group---end之后打印,说明不会堵塞当前线程
  • dispatch_group_notify之前的异步任务是同时在其他线程执行的,当所有任务都完成后,才执行dispatch_group_notify相关 block 中的任务
b、dispatch_group_wait

会暂停当前线程,等指定的group中任务全部完成后,才会继续执行

NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
   NSLog(@"group---begin");
   dispatch_group_t group =  dispatch_group_create();
   dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       // 追加任务 1
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
   });
   
   dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       // 追加任务 2
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
   });
   
   // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
   dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
   
   NSLog(@"group---end");

输出结果:
2020-07-14 16:09:34.610211+0800 TestOC[22992:1463372] currentThread---{number = 1, name = main}
2020-07-14 16:09:34.610327+0800 TestOC[22992:1463372] group---begin
2020-07-14 16:09:36.615548+0800 TestOC[22992:1463487] 1---{number = 8, name = (null)}
2020-07-14 16:09:36.615548+0800 TestOC[22992:1463486] 2---{number = 7, name = (null)}
2020-07-14 16:09:36.615707+0800 TestOC[22992:1463372] group---end

小结:

我们可以发现:

  • 当dispatch_group_wait之前所有任务执行完之后,才会执行dispatch_group_wait后面的操作,说明dispatch_group_wait会阻塞当前线程。
c、dispatch_group_enter、dispatch_group_leave
  • dispatch_group_enter相当于追加一个任务到group里,执行一次,相当于 group 中未执行完毕任务 +1
  • dispatch_group_leave相当于在group里面离开一个任务,执行一次,相当于 group 中未执行完毕任务 -1
  • 当group 里面未执行完毕的任务数量为0时,才会去执行dispatch_group_notify相关 block 中的任务
   NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
   NSLog(@"group---begin");
   //   使用dispatch_group_enter、dispatch_group_leave 组合
   dispatch_group_t group =  dispatch_group_create();
   
   dispatch_group_enter(group);
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       // 追加任务 1
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
       dispatch_group_leave(group);
   });
   
   dispatch_group_enter(group);
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       // 追加任务 2
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
       dispatch_group_leave(group);
   });
   
   dispatch_group_notify(group, dispatch_get_main_queue(), ^{
       // 等前面的异步任务 1、任务 2 都执行完毕后,回到主线程执行下边任务
       sleep(2);                                       // 模拟耗时操作
       NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
   });
   NSLog(@"group---end");

输出结果:
2020-07-14 16:22:55.973348+0800 TestOC[23194:1474590] currentThread---{number = 1, name = main}
2020-07-14 16:22:55.973507+0800 TestOC[23194:1474590] group---begin
2020-07-14 16:22:55.973626+0800 TestOC[23194:1474590] group---end
2020-07-14 16:22:57.977584+0800 TestOC[23194:1474710] 1---{number = 3, name = (null)}
2020-07-14 16:22:57.977614+0800 TestOC[23194:1474711] 2---{number = 7, name = (null)}
2020-07-14 16:22:59.978804+0800 TestOC[23194:1474590] 3---{number = 1, name = main}

小结:

我们可以发现:

  • 异步操作都在group---end之后打印,说明不会堵塞当前线程
  • dispatch_group_notify之前的异步任务是同时在其他线程执行的,当所有任务都完成后,才执行dispatch_group_notify相关 block 中的任务
  • 需要注意dispatch_group_enter和dispatch_group_leave要成对出现,dispatch_group_enter少了程序会直接崩溃,dispatch_group_leave少了这会卡死在group里面出不来
3.8.6、 GCD 信号量:dispatch_semaphore
a、控制同时最多并发量

有时候我们会遇到这种需求,如视频下载,同时最多可以下载5个视频。这是我们就可以使用dispatch_semaphore,让加在队列中的任务一次最多同时执行5个,剩下的在后面等待,等前面的执行的任务有执行完毕的,再将等待的任务追加到执行。
在 Dispatch Semaphore 中,使用计数来完成这个功能,计数小于 0 时等待,不可通过。计数为 0 或大于 0 时,计数减 1 且不等待,直接通过。
Dispatch Semaphore 提供了三个方法:
dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量
dispatch_semaphore_signal:发送一个信号,让信号总量加 1
dispatch_semaphore_wait:可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。
注意:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。

   NSLog(@"group---begin");
   //   使用dispatch_group_enter、dispatch_group_leave 组合
   dispatch_group_t group =  dispatch_group_create();
   dispatch_semaphore_t fileSemaphore = dispatch_semaphore_create(5);     //同时处理5个
   for (NSInteger i = 0; i < 10; i ++) {
       dispatch_semaphore_wait(fileSemaphore, DISPATCH_TIME_FOREVER);
       dispatch_group_enter(group);
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           
           sleep(2);            // 模拟耗时操作
           NSLog(@"%ld---%@",i,[NSThread currentThread]);
           dispatch_group_leave(group);
           dispatch_semaphore_signal(fileSemaphore);
       });
   }
   
   dispatch_group_notify(group, dispatch_get_main_queue(), ^{
       // 等前面的异步任务都执行完毕后,回到主线程执行下边任务
       NSLog(@"group---end");
   });

输出结果:
2020-07-14 18:18:34.603300+0800 TestOC[24509:1548769] group---begin
2020-07-14 18:18:36.604181+0800 TestOC[24509:1548875] 3---{number = 7, name = (null)}
2020-07-14 18:18:36.604199+0800 TestOC[24509:1548881] 1---{number = 6, name = (null)}
2020-07-14 18:18:36.604200+0800 TestOC[24509:1548876] 4---{number = 4, name = (null)}
2020-07-14 18:18:36.604213+0800 TestOC[24509:1548882] 2---{number = 3, name = (null)}
2020-07-14 18:18:36.604215+0800 TestOC[24509:1548880] 0---{number = 5, name = (null)}
2020-07-14 18:18:38.608774+0800 TestOC[24509:1548876] 6---{number = 4, name = (null)}
2020-07-14 18:18:38.608774+0800 TestOC[24509:1548881] 7---{number = 6, name = (null)}
2020-07-14 18:18:38.608774+0800 TestOC[24509:1548880] 8---{number = 5, name = (null)}
2020-07-14 18:18:38.608797+0800 TestOC[24509:1548875] 9---{number = 7, name = (null)}
2020-07-14 18:18:38.608781+0800 TestOC[24509:1548882] 5---{number = 3, name = (null)}
2020-07-14 18:18:38.609333+0800 TestOC[24509:1548769] group---end

小结:

我们可以发现:

  • 创建的异步任务一次最多同时执行5个
  • 主要用于控制最多的并发量,保证线程安全,为线程加锁
b、线程安全

Dispatch Semaphore实现线程安全就是将异步并发在某一时刻变成同步,也就是为线程加锁。
比喻现在有100张火车票,现在有100个人同时购买,如果不实现线程安全直接实现,代码如下

NSLog(@"group---begin");
   dispatch_group_t group =  dispatch_group_create();
   _dataInt = 100;
   for (NSInteger i = 0; i < 100; i ++) {
       dispatch_group_enter(group);
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           
           sleep(2);            // 模拟耗时操作
           self.dataInt --;
           NSLog(@"%ld",self.dataInt);
           dispatch_group_leave(group);
       });
   }
   
   dispatch_group_notify(group, dispatch_get_main_queue(), ^{
       // 票买完了,回到主线程查看是否还有余票
       NSLog(@"打印剩余票数---%ld",self.dataInt);
       NSLog(@"group---end");
   });

dispatch_group_notify里面是不是应该打印0,但是在时间测试中,并不是

输出结果:
2020-07-15 10:22:26.427131+0800 TestOC[26758:1684612] group---begin
2020-07-15 10:22:28.429319+0800 TestOC[26758:1685009] 99
2020-07-15 10:22:28.429341+0800 TestOC[26758:1685391] 99
2020-07-15 10:22:28.433020+0800 TestOC[26758:1685390] 97
2020-07-15 10:22:28.433039+0800 TestOC[26758:1685392] 98
2020-07-15 10:22:28.433073+0800 TestOC[26758:1685426] 96
2020-07-15 10:22:28.433096+0800 TestOC[26758:1685393] 98
...
2020-07-15 10:22:30.454582+0800 TestOC[26758:1685476] 14
2020-07-15 10:22:30.454581+0800 TestOC[26758:1685477] 13
2020-07-15 10:22:30.454589+0800 TestOC[26758:1685481] 11
2020-07-15 10:22:30.454538+0800 TestOC[26758:1685443] 21
2020-07-15 10:22:30.454554+0800 TestOC[26758:1685446] 20
2020-07-15 10:22:30.465575+0800 TestOC[26758:1684612] 打印剩余票数---11
2020-07-15 10:22:30.465780+0800 TestOC[26758:1684612] group---end

小结:

我们可以发现:

  • 我们预计的打印(或者说我们期待的打印)应该是0到99一个数打印一次,然后打印剩余票数为0,但是实际结果确实0到99到数字有点打印多次,然后打印剩余票数也不为0
  • 此时输出结果跟我们预期结果不一样,这就是线程不安全导致的
现在我们用dispatch_semaphore来实现一下线程安全
NSLog(@"group---begin");
   dispatch_group_t group =  dispatch_group_create();
   _dataInt = 100;
   dispatch_semaphore_t fileSemaphore = dispatch_semaphore_create(1);
   for (NSInteger i = 0; i < 100; i ++) {
       dispatch_group_enter(group);
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
           
           sleep(2);            // 模拟耗时操作
           dispatch_semaphore_wait(fileSemaphore, DISPATCH_TIME_FOREVER);
           self.dataInt --;
           NSLog(@"%ld",self.dataInt);
           dispatch_semaphore_signal(fileSemaphore);
           dispatch_group_leave(group);
       });
   }
   
   dispatch_group_notify(group, dispatch_get_main_queue(), ^{
       // 票买完了,回到主线程查看是否还有余票
       NSLog(@"打印剩余票数---%ld",self.dataInt);
       NSLog(@"group---end");
   });

输出结果:
2020-07-15 10:35:45.916742+0800 TestOC[26880:1694893] group---begin
2020-07-15 10:35:47.921142+0800 TestOC[26880:1695014] 99
2020-07-15 10:35:47.921701+0800 TestOC[26880:1695058] 98
2020-07-15 10:35:47.922048+0800 TestOC[26880:1695003] 97
2020-07-15 10:35:47.922335+0800 TestOC[26880:1695012] 96
2020-07-15 10:35:47.922648+0800 TestOC[26880:1695059] 95
...
2020-07-15 10:35:49.950237+0800 TestOC[26880:1695100] 4
2020-07-15 10:35:49.950387+0800 TestOC[26880:1695102] 3
2020-07-15 10:35:49.950507+0800 TestOC[26880:1695103] 2
2020-07-15 10:35:49.950770+0800 TestOC[26880:1695104] 1
2020-07-15 10:35:49.951149+0800 TestOC[26880:1695101] 0
2020-07-15 10:35:49.951582+0800 TestOC[26880:1694893] 打印剩余票数---0
2020-07-15 10:35:49.951890+0800 TestOC[26880:1694893] group---end

小结:

我们可以发现:

  • 在使用dispatch_semaphore保证线程安全后,得到的数据是正确的,并且耗时操作还是异步并发进行的,只是在扣除票数的这一部分才变成同步,总体耗时并没有发生太大变化
第一次写这种长篇分享,如果里面有不合理的地方欢迎在评论区提出。

你可能感兴趣的:(iOS多线程)