Skip to content

Latest commit

 

History

History
2125 lines (1462 loc) · 81 KB

多线程.md

File metadata and controls

2125 lines (1462 loc) · 81 KB

多线程

1 基础知识

1.1 进程

进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。


1.2 线程

1个进程想要执行任务,必须得有线程,也就是每1个进程至少要有1条线程,称为主线程。一个进程的所有任务都在线程中执行。


1.3 进程和线程的比较

线程是CPU调用(执行任务)的最小单位。

进程是CPU分配资源的最小单位。

一个进程中至少要有一个线程。

同一个进程内的线程共享进程的资源。


1.4 线程的串行

1个线程中任务的执行是串行的,如果要在1个线程中执行多个任务,那么只能一个一个按顺序执行。也就是同一时间内,1个线程只能执行1个任务。


1.5 多线程

1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务,多线程技术可以提高程序的执行效率。


1.6 多线程原理

同一时间,CPU只能处理1条线程,只有1条线程在工作(执行),多线程并发(同时)执行不同的任务,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。

如果开启的线程非常非常多,CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源,同时每条线程被调用执行的频次也会降低(线程的执行效率降低)。


1.7 多线程的优缺点

优点:

  • 能适当提高程序的执行效率

  • 能适当提高资源利用率(CPU、内存利用率)

缺点:

  • 创建线程是有开销的,iOS下主要成本包括:内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,也可以使用-setStackSize:设置,但必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间。

  • 如果开启大量的线程,会降低程序的性能,线程越多,CPU在调度线程上的开销就越大。

  • 程序设计更加复杂:比如线程之间的通信、多线程的数据共享等问题。


1.8 多线程的应用

主线程的主要作用

显示/刷新UI界面

处理UI事件(比如点击、滚动、拖拽事件等)

主线程使用注意:

别将比较耗时的操作放到主线程中

耗时操作会卡住主线程,严重影响UI的流程度,影响用户体验。

将耗时操作放在子线程执行,然后到刷新UI时再换回主线程。

1.9 iOS的4种多线程方案

方案 简介 语言 线程生命周期 使用频率
pthread 一套通用的多线程API
适用于Unix\Linux\Windows等系统
跨平台\可移植
使用难度大
C 程序员管理 几乎不用
NSThread 使用更加面向对象
简单易用,可直接操作线程对象
OC 程序员管理 偶尔使用
GCD 旨在替代NSThread等线程技术 C 自动管理 经常使用
NSOperation 基于GCD封装
比GCD多了一些更简单实用的功能
实用更加面向对象
OC 自动管理 经常使用

2 pthread

2.1 pthread简介

pthread是一套通用的多线程的API,可以在Unix/Linux/Windows等系统跨平台使用,使用C语言编写,需要程序员管理线程的生命周期,使用难度大,iOS开发中几乎不使用pthread。仅做了解。

2.2 pthread使用

需要引入头文件#import <pthread.h>

需要创建线程并开启线程任务

代码如下:

- (void)pthreadRun {
    // 创建线程 定义pthread_t变量
    pthread_t thread;
    // 开启线程 执行任务
    pthread_create(&thread, NULL, run, NULL);
    // 设置子线程的状态设置为detached,该线程运行结束后会自动释放所有资源
    pthread_detach(thread);
}

void *run(void *param) {
    NSLog(@"pthread - %@", [NSThread currentThread]);
    
    return NULL;
}

其中pthread_create(&thread, NULL, run, NULL); 的参数定义:

  • 第一个参数&thread是指线程对象,指向线程标识符的指针
  • 第二个是线程属性,可赋值NULL
  • 第三个run是指向函数的指针(run对应函数里是需要在新线程中执行的任务)
  • 第四个是运行函数的参数,可赋值NULL

2.3 pthread其他相关方法

pthread_create() 创建一个线程
pthread_exit() 终止当前线程
pthread_cancel() 中断另外一个线程的运行
pthread_join() 阻塞当前的线程,直到另外一个线程运行结束
pthread_attr_init() 初始化线程的属性
pthread_attr_setdetachstate() 设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
pthread_attr_getdetachstate() 获取脱离状态的属性
pthread_attr_destroy() 删除线程的属性
pthread_kill() 向线程发送一个信号,线程间发送消息

3 NSThread

3.1 NSThread简介

NSThread是苹果官方提供的,使用起来比pthread更加面向对象,简单易用,可以直接操作线程对象,不过也需要程序员自己管理线程的生命周期(主要是创建),iOS开发过程中偶尔使用NSThread,比如查询当前线程[NSThread currentThread];来显示当前线程,或者[NSThread isMainThread];来判断当前线程是不是主线程。


3.2 NSThread使用

需要创建线程并启动线程

代码如下:

// 创建并启动
- (void)nsthreadRun {
    // 创建线程 - 关联方法
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(nsthreadRunAction) object:nil];
    // 启动线程
    [thread start];
}

- (void)nsthreadRunAction {
    NSLog(@"NSThread - %@", [NSThread currentThread]);
}

也可以创建线程后自动启动

- (void)nsthreadRun {
    // 创建线程后自动执行方法
    [NSThread detachNewThreadSelector:@selector(nsthreadRunAction) toTarget:self withObject:nil];
    
}

或者隐式创建并启动线程

- (void)nsthreadRun {
    // 隐式创建并启动线程 该方法是NSObject的分类NSThreadPerformAdditions中实现的
    [self performSelectorInBackground:@selector(nsthreadRunAction) withObject:nil];
    
}

3.3 部分接口

// 获取主线程
+ (NSThread *)mainThread;

// 判断是否是主线程-实例方法
- (BOOL)isMainThread;

// 判断是否是主线程-类方法
+ (BOOL)isMainThread;

// 获取当前线程
[NSThread currentThread];

// name的获取和设置方法
@property (nullable, copy) NSString *name;

// 启动线程
- (void)start;

// 阻塞线程方法
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

// 强制停止线程 线程将进入死亡状态
+ (void)exit;

3.4 线程间通讯

开发中,我们经常需要在子线程做复杂操作,操作结束后再换到主线程刷新UI。

下面通过子线程下载图片,然后主线程显示的例子来说明,代码如下:

- (void)dosomeAsyncAction {
    [NSThread detachNewThreadSelector:@selector(donwlaodImage) toTarget:self withObject:nil];
}

- (void)donwlaodImage {
    // 耗时操作 子线程执行
    NSURL *imageUrl = [NSURL URLWithString:@"https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1764734306,495704103&fm=26&gp=0.jpg"];
    
    NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
    
    UIImage *image = [UIImage imageWithData:imageData];
    
    // 回到主线程
    [self performSelectorOnMainThread:@selector(renderImageView:) withObject:image waitUntilDone:NO];
    
}

- (void)renderImageView:(UIImage *)image {
    NSLog(@"renderImageView - %@", [NSThread currentThread]);
    // 更新UI
    self.imageView.image = image;
}


3.5 NSThread线程安全和线程同步

线程安全:如果你的代码所在的进程中有多个线程在同时进行,这些线程可能会同时运行同一段代码,如果每次运行结果和单线程去运行的结果是一样的,而且其中变量的值也和预期的是一样的,那就是线程安全的。

如果每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的。如果有多个线程同时操作写操作,更改其值,一般都需要考虑线程同步的情况,否则就可能影响线程安全。

线程同步:可以理解成线程A和线程B一块配合,A执行到一定程度时要依靠线程B的某个结果,于是停下来,让B运行,此时B运行后将结果给到A,A再继续操作。

举个例子:两个人一起聊天,两个人不能同时说话,避免听不清的情况,等一个人说完(一个线程结束),另外一个人再说(另一个线程再开始)。


下面,通过火车票售票的方式,实现NSThread线程安全和解决线程同步问题。

场景:一共有50张火车票,有两个地方的售票口。这两个窗口同时售卖火车票,售完为止。

3.5.1 线程不安全

先来看下正常不加锁的情况下:

- (void)threadSafe {
    // 初始50张票
    self.ticketCount = 50;
    
    // 两个售票口
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(buyTicket) object:nil];
    thread1.name = @"售票口1";
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(buyTicket) object:nil];
    thread2.name = @"售票口2";
    
    // 开始售卖票
    [thread1 start];
    [thread2 start];
    
}

- (void)buyTicket {
    while (1) {
        if (self.ticketCount > 0) {
            self.ticketCount--;
            [NSThread sleepForTimeInterval:0.2
             ];
            NSLog(@"剩余票数为:%d  thread:%@", self.ticketCount, [NSThread currentThread]);
        }
        if (self.ticketCount <= 0) {
            NSLog(@"票卖完了");
            break;
        }
    }
}

输出如下:

2020-07-11 11:14:03.205568+0800 test19[2267:125975] 剩余票数为:48  thread:<NSThread: 0x600003cc0240>{number = 8, name = 售票口2}
2020-07-11 11:14:03.205502+0800 test19[2267:125974] 剩余票数为:48  thread:<NSThread: 0x600003cc0300>{number = 7, name = 售票口1}
2020-07-11 11:14:03.407066+0800 test19[2267:125974] 剩余票数为:46  thread:<NSThread: 0x600003cc0300>{number = 7, name = 售票口1}
2020-07-11 11:14:03.407072+0800 test19[2267:125975] 剩余票数为:46  thread:<NSThread: 0x600003cc0240>{number = 8, name = 售票口2}
......
2020-07-11 11:14:08.478901+0800 test19[2267:125974] 剩余票数为:1  thread:<NSThread: 0x600003cc0300>{number = 7, name = 售票口1}
2020-07-11 11:14:08.479106+0800 test19[2267:125974] 票卖完了
2020-07-11 11:14:08.680280+0800 test19[2267:125975] 剩余票数为:0  thread:<NSThread: 0x600003cc0240>{number = 8, name = 售票口2}
2020-07-11 11:14:08.680526+0800 test19[2267:125975] 票卖完了

从输出看,票数是乱的,显然不符合预期要求,所以需要考虑线程安全的问题。

3.5.1 线程安全

线程安全的解决方案一般是给线程加锁,在一个线程执行该操作时,不允许其他线程进行操作。

iOS实现线程加锁有多种方式:@synchronized、NSLock...等等,加锁后续会用单独的篇章进行梳理。

现在先用NSLock进行加锁处理。

代码如下:

- (void)threadSafe {
    
    // 初始化NSLock
    self.lock = [[NSLock alloc] init];
    
    // 初始50张票
    self.ticketCount = 50;
    
    // 两个售票口
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(buyTicket) object:nil];
    thread1.name = @"售票口1";
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(buyTicket) object:nil];
    thread2.name = @"售票口2";
    
    // 开始售卖票
    [thread1 start];
    [thread2 start];
    
}

- (void)buyTicket {
    while (1) {
    		// 加锁
        [self.lock lock];
        if (self.ticketCount > 0) {
            self.ticketCount--;
            [NSThread sleepForTimeInterval:0.1
             ];
            NSLog(@"剩余票数为:%d  thread:%@", self.ticketCount, [NSThread currentThread]);
        }
        // 解锁
        [self.lock unlock];
        
        if (self.ticketCount <= 0) {
            NSLog(@"票卖完了");
            break;
        }
    }
}

输出如下:

2020-07-11 11:19:22.930366+0800 test19[2300:131552] 剩余票数为:49  thread:<NSThread: 0x600002fec300>{number = 7, name = 售票口1}
2020-07-11 11:19:23.036924+0800 test19[2300:131552] 剩余票数为:48  thread:<NSThread: 0x600002fec300>{number = 7, name = 售票口1}
2020-07-11 11:19:23.139949+0800 test19[2300:131552] 剩余票数为:47  thread:<NSThread: 0x600002fec300>{number = 7, name = 售票口1}
2020-07-11 11:19:23.245209+0800 test19[2300:131552] 剩余票数为:46  thread:<NSThread: 0x600002fec300>{number = 7, name = 售票口1}
......
2020-07-11 11:19:27.358242+0800 test19[2300:131553] 剩余票数为:6  thread:<NSThread: 0x600002fec240>{number = 8, name = 售票口2}
2020-07-11 11:19:27.460865+0800 test19[2300:131552] 剩余票数为:5  thread:<NSThread: 0x600002fec300>{number = 7, name = 售票口1}
2020-07-11 11:19:27.562626+0800 test19[2300:131552] 剩余票数为:4  thread:<NSThread: 0x600002fec300>{number = 7, name = 售票口1}
2020-07-11 11:19:27.664041+0800 test19[2300:131552] 剩余票数为:3  thread:<NSThread: 0x600002fec300>{number = 7, name = 售票口1}
2020-07-11 11:19:27.764513+0800 test19[2300:131552] 剩余票数为:2  thread:<NSThread: 0x600002fec300>{number = 7, name = 售票口1}
2020-07-11 11:19:27.866442+0800 test19[2300:131552] 剩余票数为:1  thread:<NSThread: 0x600002fec300>{number = 7, name = 售票口1}
2020-07-11 11:19:27.966969+0800 test19[2300:131552] 剩余票数为:0  thread:<NSThread: 0x600002fec300>{number = 7, name = 售票口1}
2020-07-11 11:19:27.967307+0800 test19[2300:131552] 票卖完了
2020-07-11 11:19:27.967419+0800 test19[2300:131553] 票卖完了

加锁后,得到的剩余票数是正确的,解决了多个线程同步的问题。


3.6 线程的状态转换

当新建一条线程时,在内存中的表现为:

当调用启动线程后,系统把线程对象放入可调度线程池中,线程对象进入就绪状态,如下图:

同时,可调度线程池中可能有其他线程对象,如图:

线程的状态转换:

  • 如果CPU现在调度当前线程对象,则当前线程对象进入运行状态,如果CPU调度其他线程对象,则当前线程对象回到就绪状态。
  • 如果CPU在运行当前线程对象的时候调用了sleep方法\等待同步锁,则当前线程对象就进入了人阻塞状态,等到sleep到时\得到同步锁,则回到就绪状态。
  • 如果CPU在运行当前线程对象的时候线程任务执行完毕\异常强制退出,则当前线程对象进入死亡状态。

整个过程如下图:


4 NSOperation

4.1 NSOperation、NSOperationQueue简介

NSOperation、NSOperationQueue是苹果提供给我们的一套多线程解决方案,实际上NSOperation、NSOperationQueue是基于GCD更高一层的封装,完全面向对象。但是比GCD要更简单易用、代码可读性也更高。

使用NSOperation、NSOperationQueue的优点

  • 可添加完成的代码块,在操作完成后执行。
  • 添加操作之间的依赖关系,方便的控制执行顺序。
  • 设定操作执行的优先级。
  • 可以很方便的取消一个操作的执行。
  • 使用KVO观察线程操作对象执行的状态改变:isExecuting、isFinished、isCancelled。

4.2 NSOperation、NSOperationQueue操作和操作队列

既然是基于GCD的封装,那么GCD中的一些概念同样适用于NSOperation、NSOperationQueue。也存在类似的任务(操作)和队列(操作队列)。

操作(Operation)

  • 执行操作,就是在线程中执行指定的代码。
  • 在GCD中是放在block中的,在NSOperation中,是在其子类NSInvocationOperation、NSBlockOperation,或者自定义子类来封装。

操作队列(Operation Queues)

  • 这里的队列指操作队列,即用来存放操作的队列。不同于GCD中的调度队列先进先出的原则。对于NSOperationQueue添加到队列中的操作,首先进入准备就绪的状态,就绪状态取决于操作之间的依赖关系。然后进入就绪状态的操作开始执行顺序,是由操作之间的优先级决定的,优先级是操作自身的一个属性。
  • 操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发或是串行。
  • NSOperationQueue为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。

4.3 NSOperation使用

NSOperation需要配合NSOperationQueue来实现多线程,因为默认情况下,NSOperation单独使用时系统同步执行操作,配合NSOperationQueue我们能更好的实现异步执行。

NSOperation实现多线程的使用步骤分为三步:

  1. 创建操作:先将需要执行的操作封装到一个NSOperation对象中。
  2. 创建队列:创建NSOperationQueue对象。
  3. 将操作添加到队列中:将NSOperation对象添加到NSOperationQueue对象中。

创建队列:创建NSOperationQueue对象。

将操作加入到队列中:将NSOperation对象添加到NSOperationQueue对象中。

之后,系统就会自动将NSOperationQueue中的NSOperation对象取出来,在新线程中执行操作。

下面来通过代码具体实现:

首先是封装NSOperation对象,但是不能直接使用NSOperation对象来进行实例化操作,因为是个抽象类,需要使用它的子类来实例化操作。有以下三种方式封装:

  • 使用子类NSInvocationOperation
  • 使用子类NSBlockOperation
  • 使用自定义的继承自NSOperation的子类,然后内部实现相应的方法来封装。

在不使用NSOperationQueue的情况下,系统是会同步执行。三种方式的代码如下:


4.3.1 NSInvocationOperation方式
- (void)operationRun {
    // NSInvocationOperation方式
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
     
    [op start];
}

- (void)task1 {
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"1---%@", [NSThread currentThread]);
    }
}

输出如下:

2020-07-11 12:58:39.988404+0800 test19[3521:198301] 1---<NSThread: 0x6000008f6dc0>{number = 1, name = main}
2020-07-11 12:58:41.989366+0800 test19[3521:198301] 1---<NSThread: 0x6000008f6dc0>{number = 1, name = main}

执行的时候,会卡一下,因为sleep的时候是在主线程,从输出的内容也可以看出是在主线程中。

如果在其他线程中执行该操作,输出的也是当前执行的线程,并不会开启新线程。

[NSThread detachNewThreadSelector:@selector(operationRun) toTarget:self withObject:nil];

输出如下:

2020-07-11 13:06:47.471999+0800 test19[3594:210772] 1---<NSThread: 0x600003774300>{number = 7, name = (null)}
2020-07-11 13:06:49.475836+0800 test19[3594:210772] 1---<NSThread: 0x600003774300>{number = 7, name = (null)}

4.3.2 NSBlockOperation方式

使用NSBlockOperation方式,可以直接定义代码块,并执行,同样的也是在当前线程中执行,不会开启新的线程执行。

- (void)operationRun {    
    // NSBlockOperation
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@", [NSThread currentThread]);
        }
    }];

    [op start];
}

输出如下:

2020-07-11 13:06:47.471999+0800 test19[3594:210772] 1---<NSThread: 0x600003774300>{number = 7, name = (null)}
2020-07-11 13:06:49.475836+0800 test19[3594:210772] 1---<NSThread: 0x600003774300>{number = 7, name = (null)}

同时比上小节的NSInvocationOperation,多了可以往对象里添加代码块的功能。添加的这些操作可以在不同的操作中并发执行,只有当所有相关的操作完成时,才视为完成。

- (void)operationRun {    
    // NSBlockOperation
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@", [NSThread currentThread]);
        }
    }];
    
    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2---%@", [NSThread currentThread]);
        }
    }];

    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3---%@", [NSThread currentThread]);
        }
    }];

    [op addExecutionBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"4---%@", [NSThread currentThread]);
        }
    }];

    [op start];
}

输出如下:

2020-07-11 13:14:48.614789+0800 test19[3637:221405] 2---<NSThread: 0x600001c08cc0>{number = 5, name = (null)}
2020-07-11 13:14:48.614781+0800 test19[3637:221212] 1---<NSThread: 0x600001c52100>{number = 1, name = main}
2020-07-11 13:14:48.614800+0800 test19[3637:221406] 3---<NSThread: 0x600001c08600>{number = 6, name = (null)}
2020-07-11 13:14:48.614800+0800 test19[3637:221402] 4---<NSThread: 0x600001c250c0>{number = 7, name = (null)}
2020-07-11 13:14:50.616413+0800 test19[3637:221402] 4---<NSThread: 0x600001c250c0>{number = 7, name = (null)}
2020-07-11 13:14:50.616413+0800 test19[3637:221406] 3---<NSThread: 0x600001c08600>{number = 6, name = (null)}
2020-07-11 13:14:50.616412+0800 test19[3637:221212] 1---<NSThread: 0x600001c52100>{number = 1, name = main}
2020-07-11 13:14:50.616412+0800 test19[3637:221405] 2---<NSThread: 0x600001c08cc0>{number = 5, name = (null)}

添加的操作多,就会自动开启新线程,开启的数量由系统来决定。


4.3.3 继承自NSOperation的子类

如果上述两种方式都不能满足开发的需求,那么我们可以使用自定义的继承自NSOperation的子类,重写main或者start方法来定义自己的NSOperation对象。重写main的方法比较简单,我们不需要管理操作的状态属性isExecuting和isFinished。当main执行完返回的时候,这个操作就结束了。

先定义一个继承自NSOperation的子类,重写main方法。

// .h
@interface TSHOperation : NSOperation

@end

// .m
#import "TSHOperation.h"

@implementation TSHOperation


- (void)main {
    if (!self.isCancelled) {
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@", [NSThread currentThread]);
        }
    }
}

@end

然后在需要的地方调用,代码如下:

- (void)operationRun {    
    // NSOperation 自定义子类
    TSHOperation *op = [[TSHOperation alloc] init];
     
    [op start];
}

输出如下:

2020-07-11 13:23:05.006789+0800 test19[3701:229904] 1---<NSThread: 0x600000bc9040>{number = 1, name = main}
2020-07-11 13:23:07.008280+0800 test19[3701:229904] 1---<NSThread: 0x600000bc9040>{number = 1, name = main}

4.4 配合NSOperationQueue使用

NSOperationQueue一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并行功能。

主队列,添加到主队列中的操作都会放到主线程中执行

// 主队列
NSOperationQueue *queue = [NSOperationQueue mainQueue];

自定义队列,添加到此队列中的操作,会自动放到子线程中执行,同时包含了串行和并发功能。

// 非主队列 同时包含了串行和并行
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

上述中说到,将操作加入到队列中,实现多线程操作。

加入到队列中有两种方法:

第一种是需要添加NSOperation对象

- (void)addOperation:(NSOperation *)op;

此种方式需要有NSOperation的对象,也就是上小节中说的三种封装方式的对象。

// 创建自定义队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 操作1
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 操作2
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];
// 添加到队列中
[queue addOperation:op1];
[queue addOperation:op2];

- (void)task1 {
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"1---%@", [NSThread currentThread]);
    }
}

- (void)task2 {
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2---%@", [NSThread currentThread]);
    }
}

输出如下:

2020-07-11 13:32:43.996250+0800 test19[3738:242957] 2---<NSThread: 0x600001500280>{number = 6, name = (null)}
2020-07-11 13:32:43.996250+0800 test19[3738:242958] 1---<NSThread: 0x60000152fcc0>{number = 7, name = (null)}
2020-07-11 13:32:45.999699+0800 test19[3738:242957] 2---<NSThread: 0x600001500280>{number = 6, name = (null)}
2020-07-11 13:32:45.999704+0800 test19[3738:242958] 1---<NSThread: 0x60000152fcc0>{number = 7, name = (null)}

可以看到,添加到自定义队列中的操作是在子线程中执行的。


第二种是需要使用block

- (void)addOperationWithBlock:(void (^)(void))block;

无需先创建操作,在block中添加操作代码,直接将包含操作的block加入到队列中。

// 创建自定义队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"5---%@", [NSThread currentThread]);
    }
}];

[queue addOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"6---%@", [NSThread currentThread]);
    }
}];

输出如下:

2020-07-11 13:40:51.788120+0800 test19[3800:253116] 5---<NSThread: 0x600003cbbac0>{number = 6, name = (null)}
2020-07-11 13:40:51.788193+0800 test19[3800:252888] 6---<NSThread: 0x600003c92880>{number = 3, name = (null)}
2020-07-11 13:40:53.792429+0800 test19[3800:253116] 5---<NSThread: 0x600003cbbac0>{number = 6, name = (null)}
2020-07-11 13:40:53.792433+0800 test19[3800:252888] 6---<NSThread: 0x600003c92880>{number = 3, name = (null)}

从输出中可以看出通过该方式添加到队列后也是开启新线程并发执行。


4.4.1 通过NSOperationQueue控制串行、并发

在NSOperationQueue有个属性maxConcurrentOperationCount,最大并发操作数,通过这个属性来控制特定队列中可以有多少个操作同时参与并发执行。

这里的属性控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。

该属性有三种情况:

  • 默认情况下是-1,表示不进行限制,可进行并发执行。
  • 设为1时,队列为串行队列,只能串行执行。
  • 大于1时,队列为并发队列。操作并发队列执行,当然这个值不能超过系统限制,即使设成很大的数,系统也会自动调整成系统设定的默认最大值。

通过设置代码如下:

- (void)operationQueueRun {
    // 创建队列
    // 主队列
//    NSOperationQueue *queue = [NSOperationQueue mainQueue];
    // 非主队列 同时包含了串行和并行
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 设置最大并发数 默认为-1 设为1为串行 大于1为并行
    queue.maxConcurrentOperationCount = 1;
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];

    
    [queue addOperation:op1];
    [queue addOperation:op2];
}

输出如下:

2020-07-11 13:48:02.954888+0800 test19[3841:260280] 1---<NSThread: 0x6000006ba780>{number = 4, name = (null)}
2020-07-11 13:48:04.956913+0800 test19[3841:260280] 1---<NSThread: 0x6000006ba780>{number = 4, name = (null)}
2020-07-11 13:48:06.959077+0800 test19[3841:260281] 2---<NSThread: 0x6000006dab80>{number = 7, name = (null)}
2020-07-11 13:48:08.964802+0800 test19[3841:260281] 2---<NSThread: 0x6000006dab80>{number = 7, name = (null)}

会变成串行顺序输出。上面代码的属性maxConcurrentOperationCount设为2时,输出如下:

2020-07-11 13:49:10.776596+0800 test19[3854:262126] 1---<NSThread: 0x600001e3d140>{number = 4, name = (null)}
2020-07-11 13:49:10.776596+0800 test19[3854:262127] 2---<NSThread: 0x600001e028c0>{number = 5, name = (null)}
2020-07-11 13:49:12.782159+0800 test19[3854:262126] 1---<NSThread: 0x600001e3d140>{number = 4, name = (null)}
2020-07-11 13:49:12.782153+0800 test19[3854:262127] 2---<NSThread: 0x600001e028c0>{number = 5, name = (null)}

大于1则又变成了并发执行。而开启线程数量是由系统决定的,不需要我们来管理。


4.5 NSOperation操作依赖

上面的简介中,有说到NSOperation可以添加依赖。具体实现时是通过3个接口来管理和查看依赖:

// 添加依赖
- (void)addDependency:(NSOperation *)op;

// 移除依赖
- (void)removeDependency:(NSOperation *)op;

// 查看所有依赖操作对象数组
@property (readonly, copy) NSArray<NSOperation *> *dependencies;

通过代码来看下两个操作依赖,A依赖于B执行结束,如下:

- (void)operationDependeRun {
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];
    
    //依赖完第一个操作结束后再进行
    [op1 addDependency:op2];
    
    [queue addOperation:op1];
    [queue addOperation:op2];
}

输出如下:

2020-07-11 13:54:08.296090+0800 test19[3854:262125] 2---<NSThread: 0x600001e3e400>{number = 3, name = (null)}
2020-07-11 13:54:10.296701+0800 test19[3854:262125] 2---<NSThread: 0x600001e3e400>{number = 3, name = (null)}
2020-07-11 13:54:12.302419+0800 test19[3854:267063] 1---<NSThread: 0x600001e27080>{number = 6, name = (null)}
2020-07-11 13:54:14.308065+0800 test19[3854:267063] 1---<NSThread: 0x600001e27080>{number = 6, name = (null)}

可以看到,是在task2的输出结束之后才开始输出task1的输出。


4.6 NSOperation优先级

NSOperation提供了优先级的属性queuePriority,通常操作中,默认情况下都是Normal的优先级,可以通过setQueuePriority来改变当前操作的优先级,一共有5个等级:

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
	NSOperationQueuePriorityVeryLow = -8L,
	NSOperationQueuePriorityLow = -4L,
	NSOperationQueuePriorityNormal = 0,
	NSOperationQueuePriorityHigh = 4,
	NSOperationQueuePriorityVeryHigh = 8
};

前面有说:对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间的相对优先级决定。

当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。

举个例子:现在有4个优先级都是NSOperationQueuePriorityNormal(默认级别)的操作:

op1、op2、op3、op4。其中op3依赖于op2,op2依赖于op1,即op3 -> op2 -> op1。现在讲这4个操作添加到队列中并发执行。

  • 因为op1和op4都没有需要依赖的操作,所以在op1、op4执行之前,就是处于准备就绪状态的操作。
  • 而op3和op2都有依赖的操作(op3依赖于op2,op2依赖于op1),所以op3和op2都不是准备就绪状态下的操作。

理解了进入就绪状态的操作,那么我们就理解了queuePriority属性的作用对象。

  • queuePriority属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。
  • 如果一个队列中既包含高优先级的操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。比如上例中,如果op1和op4是不同优先级的操作,那么就会先执行优先级高的操作。
  • 如果一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。

4.7 NSOperation、NSOperationQueue线程间的通信

将耗时操作放到子线程,操作结束之后到主线程更新UI。

代码如下:

- (void)operationCommunity {
    // 新建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 将操作添加到队列中
    [queue addOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@", [NSThread currentThread]);
        }
        // 在主队列中执行操作
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2];
                NSLog(@"2---%@", [NSThread currentThread]);
            }
        }];
    }];
}

输出如下:

2020-07-12 23:28:30.622293+0800 test19[5582:591402] 1---<NSThread: 0x60000353a540>{number = 6, name = (null)}
2020-07-12 23:28:32.626077+0800 test19[5582:591402] 1---<NSThread: 0x60000353a540>{number = 6, name = (null)}
2020-07-12 23:28:34.626968+0800 test19[5582:590505] 2---<NSThread: 0x60000355c500>{number = 1, name = main}
2020-07-12 23:28:36.627398+0800 test19[5582:590505] 2---<NSThread: 0x60000355c500>{number = 1, name = main}

从输出中看出,先在子线程执行的操作,最后回到了主线程。


4.8 NSOperation、NSOperationQueue线程安全

和NSThread一样,使用NSOperation和NSOperationQueue也会存在线程安全的问题。这里和3.5节一样,使用一样的例子进行说明。

场景:一共有50张火车票,有两个地方的售票口。这两个窗口同时售卖火车票,售完为止。


4.8.1 线程不安全

先来看下正常不加锁的情况下:

- (void)operationThreadSafe {
    NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(buyTicket) object:nil];
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(buyTicket) object:nil];
    self.ticketCount = 50;
    
    [queue1 addOperation:op1];
    [queue2 addOperation:op2];
}

- (void)buyTicket {
    while (1) {
        if (self.ticketCount > 0) {
            self.ticketCount--;
            [NSThread sleepForTimeInterval:0.1
             ];
            NSLog(@"剩余票数为:%d  thread:%@", self.ticketCount, [NSThread currentThread]);
        }
        
        if (self.ticketCount <= 0) {
            NSLog(@"票卖完了");
            break;
        }
    }
}

输出如下:

2020-07-12 23:33:22.529705+0800 test19[5614:597282] 剩余票数为:48  thread:<NSThread: 0x600001a24bc0>{number = 4, name = (null)}
2020-07-12 23:33:22.529764+0800 test19[5614:597281] 剩余票数为:48  thread:<NSThread: 0x600001a3bc00>{number = 6, name = (null)}
2020-07-12 23:33:22.635281+0800 test19[5614:597282] 剩余票数为:46  thread:<NSThread: 0x600001a24bc0>{number = 4, name = (null)}
......
2020-07-12 23:33:25.711348+0800 test19[5614:597282] 剩余票数为:1  thread:<NSThread: 0x600001a24bc0>{number = 4, name = (null)}
2020-07-12 23:33:25.813081+0800 test19[5614:597281] 剩余票数为:0  thread:<NSThread: 0x600001a3bc00>{number = 6, name = (null)}
2020-07-12 23:33:25.813081+0800 test19[5614:597282] 剩余票数为:0  thread:<NSThread: 0x600001a24bc0>{number = 4, name = (null)}
2020-07-12 23:33:25.813414+0800 test19[5614:597281] 票卖完了
2020-07-12 23:33:25.813414+0800 test19[5614:597282] 票卖完了

输出是不符合预期的

从输出看,票数是乱的,显然不符合预期要求,所以需要考虑线程安全的问题。


4.8.2 线程安全

和NSThread中一样,采用NSLock进行加锁处理。

代码如下:

- (void)operationThreadSafe {
    self.lock = [[NSLock alloc] init];
    
    NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(buyTicket) object:nil];
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(buyTicket) object:nil];
    self.ticketCount = 50;
    
    [queue1 addOperation:op1];
    [queue2 addOperation:op2];
}

- (void)buyTicket {
    while (1) {
        [self.lock lock];
        if (self.ticketCount > 0) {
            self.ticketCount--;
            [NSThread sleepForTimeInterval:0.1
             ];
            NSLog(@"剩余票数为:%d  thread:%@", self.ticketCount, [NSThread currentThread]);
        }
        [self.lock unlock];
        
        if (self.ticketCount <= 0) {
            NSLog(@"票卖完了");
            break;
        }
    }
}

输出如下:

2020-07-12 23:37:52.073192+0800 test19[5638:601694] 剩余票数为:49  thread:<NSThread: 0x600000862080>{number = 5, name = (null)}
2020-07-12 23:37:52.175340+0800 test19[5638:601694] 剩余票数为:48  thread:<NSThread: 0x600000862080>{number = 5, name = (null)}
2020-07-12 23:37:52.280336+0800 test19[5638:601694] 剩余票数为:47  thread:<NSThread: 0x600000862080>{number = 5, name = (null)}
......
2020-07-12 23:37:57.721520+0800 test19[5638:601694] 剩余票数为:2  thread:<NSThread: 0x600000862080>{number = 5, name = (null)}
2020-07-12 23:37:57.825621+0800 test19[5638:601691] 剩余票数为:1  thread:<NSThread: 0x600000836c40>{number = 7, name = (null)}
2020-07-12 23:37:57.928676+0800 test19[5638:601691] 剩余票数为:0  thread:<NSThread: 0x600000836c40>{number = 7, name = (null)}
2020-07-12 23:37:57.928922+0800 test19[5638:601691] 票卖完了
2020-07-12 23:37:57.930621+0800 test19[5638:601694] 票卖完了

加锁后,得到的剩余票数是正确的,解决了多个线程同步的问题。


4.9 NSOperation常用属性和方法

取消操作

  • - (void)cancel;可取消操作,实质是标记isCancelled状态。

判断操作状态方法

  • isFinished 判断操作是否已经结束
  • isCancelled 判断操作是否已经标记为取消
  • isExecuting 判断操作是否已经正在运行
  • isReady 判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关。

操作同步

  • - (void)waitUntilFinished; 阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步。
  • completionBlock 会在当前操作执行完毕时执行
  • - (void)addDependency:(NSOperation *)op; 添加依赖
  • - (void)removeDependency:(NSOperation *)op; 移除依赖
  • @property (readonly, copy) NSArray<NSOperation *> *dependencies; 查看所有依赖操作对象数组。

4.10 NSOperationQueue常用属性和方法

取消/暂停/恢复操作

  • - (void)cancelAllOperations; 可以取消队列的所有操作。
  • isSuspended 判断队列是否处于暂停状态,YES为暂停状态,NO为恢复状态。
  • suspended 设置操作的暂停和恢复,YES为暂停队列,NO为恢复队列。

操作同步

  • - (void)waitUntilAllOperationsAreFinished; 阻塞当前线程,直到队列中的操作全部执行完毕。
  • 添加/获取操作
  • - (void)addOperationWithBlock:(void (^)(void))block 向队列中添加一个NSBlockOperation类型操作对象。
  • - (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait 向队列中添加操作数组,wait标志是否阻塞当前线程直到所有操作结束。
  • operations 当前队列中的操作数组,某个操作执行结束后会自动从这个数组中清除。
  • operationCount 当前队列中的操作数。

获取队列

  • currentQueue 获取当前队列,如果当前线程不是在NSOperationQueue上运行则返回nil。
  • mainQueue 获取主队列。

需要注意,上述说的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。

暂停和取消的区别在于:暂停操作之后还可以恢复操作,继续向下执行,而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。


5 GCD

5.1 GCD简介

GCD(Grand Central Dispatch)是Apple开发的一个多核变成的较新的解决方法,它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在Mac OS X 10.6雪豹中首次推出,也可以在iOS4及以上版本使用。

GCD的优点如下:

GCD可用于多核的并行运算;

GCD会自动利用更多的CPU内核(比如双核、四核);

GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程);

程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。


5.2 GCD任务和队列

GCD中两个核心概念:任务队列

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

同步执行(sync):

同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。

只能在当前线程中执行任务,不具备开启新线程的能力。

异步执行(async):

异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。

可以在新的线程中执行任务,具备开启新线程的能力。

举个简单例子:你要打电话给小明和小白。同步执行就是:你打电话给小明的时候,不能同时打给小白,只能等到给小明打完了,才能打给小白(等待任务执行结束)。而且只能用当前的电话(不具备开启新线程的能力)。异步执行就是:你打电话给小明的时候,不用等着和小明通话结束(不用等待任务执行结束),还能同时给小白打电话。而且除了当前电话,你还可以使用其他一个或多个电话(具备开启新线程的能力)。

注意:异步执行虽然具有开启新线程的能力,但是并不一定开启新线程,这跟任务所执行的队列类型有关。


队列:这里的队列指执行任务的等待队列,即用来存放任务的队列,队列是一种特殊的线性表,采用先进先出的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取,每读取一个任务,则从队列中释放一个任务。

在GCD中有两种队列:串行队列和并发队列。两者都符合先进先出的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。

串行队列

每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)

并发队列

可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

并发队列的并发功能只有在异步方法(dispatch_async)下才有效。


5.3 GCD的使用步骤

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

创建一个队列(串行队列或并发队列)

将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)


5.3.1 队列的创建方法/获取方法

可以使用dispatch_queue_create方法来创建队列。该方法需要传入两个参数:

第一个参数表示队列的唯一标识符,用于DEBUG,可为空。

第二个参数用来识别是串行队列还是并发队列。

  • DISPATCH_QUEUE_SERIAL :串行队列;

  • DISPATCH_QUEUE_CONCURRENT:并行队列;

  • DISPATCH_QUEUE_SERIAL_INACTIVE:需要唤醒的串行队列,执行前需要通过dispatch_activate唤醒

  • DISPATCH_QUEUE_CONCURRENT_INACTIVE:需要唤醒的并行队列,执行前需要通过dispatch_activate唤醒

  • DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL:生成带@autoreleasepool功能的串行队列,相当于代码块包了一层@autoreleasepool

  • DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL:生成带@autoreleasepool功能的并行队列,相当于代码块包了一层@autoreleasepool

dispatch_queue_t queue1 = dispatch_queue_create("com.tsh.concurrent1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.tsh.concurrent2", DISPATCH_QUEUE_CONCURRENT);

对于串行队列,GCD默认提供了:主队列

所有放在主队列中的任务,都会放到主线程中执行。

可使用dispatch_get_main_queue();方法获得主队列。

主队列其实就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,有都会放到主线程中去执行,所以才造成了主队列特殊的现象。

dispatch_queue_t queue = dispatch_get_main_queue();

对于并发队列,GCD默认提供了全局并发队列。

可以使用dispatch_get_global_queue方法来获取全局并发队列。需要传入两个参数,第一个参数是队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT,对应的枚举是0。第二个参数暂时没用,用0即可。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

5.3.2 任务的创建方法

GCD提供了同步执行任务的创建方法dispatch_sync和异步执行任务创建方法dispatch_async。

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

虽然使用GCD只需要两步,但是既然有两种队列,两种任务执行方式,那么我们就有了四种不同的组合方式。针对全局队列和主队列,全局并发队列可以作为普通并发队列来使用,但是主队列因为代码默认放在主队列中,所以主队列有必要专门来研究。所有一共是六种组合方式:

  1. 同步执行+并发队列
  2. 异步执行+并发队列
  3. 同步执行+串行队列
  4. 异步执行+串行队列
  5. 同步执行+主队列
  6. 异步执行+主队列

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

主线程中,不同队列 + 不同任务 简单组合的区别如下:

区别 并发队列 串行队列 主队列
同步 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 死锁卡住不执行
异步 有开启新线程,并发执行任务 有开启新线程(1条),串行执行任务 没有开启新线程,串行执行任务

其中,在主线程中用主队列 + 同步执行 会导致死锁问题。这是因为主队列中追加的同步任务 和 主线程本身的任务 两者互相等待,阻塞了主队列,最终造成了主队列所在的线程(主线程)死锁问题。而如果我们在其他线程调用主队列+同步执行,则不会阻塞主队列,自然也不会造成死锁问题。最终的结果是:不会开启新线程,串行执行任务。


5.4.1 队列嵌套下,不同组合方式区别

除了上述提到的主线程中调用主队列+同步执行会导致死锁问题之外,实际在使用 串行队列的时候,也可能出现阻塞串行队列所在线程的情况发生,从而造成死锁问题。这种情况多见于同一个串行队列的嵌套使用。

比如在异步执行+串行队列中,又嵌套了用当前的串行队列来同步执行,代码如下:

dispatch_queue_t queue = dispatch_queue_create("com.tsh.serial", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue, ^{
    dispatch_sync(queue, ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    });
});

上面的代码会导致,串行队列中追加的任务和串行队列中原有的任务两者之间互相等待,阻塞了串行队列,最终造成了串行队列所在的线程死锁问题。

不同队列 + 不同任务组合,以及队列中嵌套队列使用的区别:

区别 异步执行+并发队列 嵌套 同一个并发队列 同步执行+并发队列 嵌套 同一个并发队列 异步执行+串行队列 嵌套 同一个串行队列 同步执行+串行队列 嵌套 同一个串行队列
同步 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 死锁卡住不执行 死锁卡住不执行
异步 开启新线程,并发执行任务 开启新线程,并发执行任务 有开启1条新线程,串行执行任务 有开启1条新线程,串行执行任务

5.5 GCD的使用

5.5.1 同步执行 + 并发队列

在当前线程中执行任务,不会开启新线程,串行执行任务。

代码如下:

- (void)gcdRun {
    
    NSLog(@"0----- current Thread :%@", [NSThread currentThread]);
    // 同步执行加并发队列 没有开启新线程 串行进行
    dispatch_queue_t queue = dispatch_queue_create("com.tsh.concurrent1", DISPATCH_QUEUE_CONCURRENT);

    dispatch_sync(queue, ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"3----- current Thread :%@", [NSThread currentThread]);
    });
    
    NSLog(@"E----- end Run");
}

输出如下:

2020-07-13 10:16:42.907486+0800 test19[1121:156548] 0----- current Thread :<NSThread: 0x600000676b80>{number = 1, name = main}
2020-07-13 10:16:42.907832+0800 test19[1121:156548] 1----- current Thread :<NSThread: 0x600000676b80>{number = 1, name = main}
2020-07-13 10:16:42.908129+0800 test19[1121:156548] 2----- current Thread :<NSThread: 0x600000676b80>{number = 1, name = main}
2020-07-13 10:16:42.908385+0800 test19[1121:156548] 3----- current Thread :<NSThread: 0x600000676b80>{number = 1, name = main}
2020-07-13 10:16:42.908620+0800 test19[1121:156548] E----- end Run

从上面可看出

所有任务都是在当前线程中执行的,同步执行不具备开启新线程的能力。

所有任务都是在0和end之间执行的,同步任务需要等待队列的任务执行结束。

任务按顺序执行,虽然并发队列可以开启多个线程,同时执行多个任务。但是本身不能创建新线程,只是当前线程这一个线程(同步任务不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务需要等待队列的任务执行结束)。所以任务只能按顺序执行,不能同时被执行。


5.5.2 异步执行 + 并发队列

可以开启多个线程,同时执行任务。代码如下:

- (void)gcdRun {
    
    NSLog(@"0----- current Thread :%@", [NSThread currentThread]);
    
    // 异步执行加并发队列 开启新线程 并发进行
    dispatch_queue_t queue = dispatch_queue_create("com.tsh.concurrent1", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"3----- current Thread :%@", [NSThread currentThread]);
    });
    
    NSLog(@"E----- end Run");
}

输出如下:

2020-07-13 10:30:29.071747+0800 test19[1300:188808] 0----- current Thread :<NSThread: 0x600003622140>{number = 1, name = main}
2020-07-13 10:30:29.072108+0800 test19[1300:188808] E----- end Run
2020-07-13 10:30:29.072210+0800 test19[1300:189711] 1----- current Thread :<NSThread: 0x60000365a040>{number = 6, name = (null)}
2020-07-13 10:30:29.072259+0800 test19[1300:188945] 2----- current Thread :<NSThread: 0x60000365cb40>{number = 7, name = (null)}
2020-07-13 10:30:29.072260+0800 test19[1300:188940] 3----- current Thread :<NSThread: 0x600003674dc0>{number = 8, name = (null)}

从上面可看出

除了当前线程,系统又开启了3个线程,并且任务是同时执行的(异步执行具备开启新线程的能力)。并且并发队列可以开启多个线程,同时执行多个任务。

所有任务是在0和end之后才执行的,说明当前线程没有等待,而是直接开启了新线程,在新线程中执行任务(异步执行不做等待,可以继续执行任务)。


5.5.3 同步执行 + 串行队列

不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。

- (void)gcdRun {
    
    NSLog(@"0----- current Thread :%@", [NSThread currentThread]);
    
    
    // 同步执行加串行队列 不开启新线程 顺序进行
    dispatch_queue_t queue = dispatch_queue_create("com.tsh.concurrent1", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"3----- current Thread :%@", [NSThread currentThread]);
    });
    
    NSLog(@"E----- end Run");
}

输出如下:

2020-07-13 10:40:01.131596+0800 test19[1340:220077] 0----- current Thread :<NSThread: 0x600001e02d80>{number = 1, name = main}
2020-07-13 10:40:01.131926+0800 test19[1340:220077] 1----- current Thread :<NSThread: 0x600001e02d80>{number = 1, name = main}
2020-07-13 10:40:01.132204+0800 test19[1340:220077] 2----- current Thread :<NSThread: 0x600001e02d80>{number = 1, name = main}
2020-07-13 10:40:01.132454+0800 test19[1340:220077] 3----- current Thread :<NSThread: 0x600001e02d80>{number = 1, name = main}
2020-07-13 10:40:01.132701+0800 test19[1340:220077] E----- end Run

从上述中可以看出

所有任务都是在当前线程中执行的,并没有开启新线程(同步执行不具备开启新线程的能力)。

所有任务都是在0和end之间执行(同步任务需要等待队列的任务执行结束)

任务是按顺序执行的,串行队列每次只一个任务被执行,任务一个接着一个按顺序执行。


5.5.4 异步执行 + 串行队列

会开启新线程,但是因为是串行队列,会顺序执行任务。代码如下:

- (void)gcdRun {
    
    NSLog(@"0----- current Thread :%@", [NSThread currentThread]);
    
    // 异步执行加串行队列 开启1个线程 顺序进行
    dispatch_queue_t queue = dispatch_queue_create("com.tsh.concurrent1", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"3----- current Thread :%@", [NSThread currentThread]);
    });
    
    NSLog(@"E----- end Run");
}

输出如下:

2020-07-13 10:44:57.819337+0800 test19[1485:233369] 0----- current Thread :<NSThread: 0x600001b624c0>{number = 1, name = main}
2020-07-13 10:44:57.819548+0800 test19[1485:233369] E----- end Run
2020-07-13 10:44:57.819607+0800 test19[1485:234307] 1----- current Thread :<NSThread: 0x600001b1d100>{number = 7, name = (null)}
2020-07-13 10:44:57.819755+0800 test19[1485:234307] 2----- current Thread :<NSThread: 0x600001b1d100>{number = 7, name = (null)}
2020-07-13 10:44:57.819929+0800 test19[1485:234307] 3----- current Thread :<NSThread: 0x600001b1d100>{number = 7, name = (null)}

从上述中可以看出

开启了一条新线程(异步执行具备开启新线程的能力,串行队列只开启一个线程)。

所有任务是在0和end之后才开始执行(异步执行不会做任何等待,可以继续执行任务)。

任务是按顺序执行的(串行队列每次只有一个任务被执行,顺序执行任务)。


5.5.5 同步执行 + 主队列

同步执行 + 主队列在不同线程中调用结果是不一样的,在主线程中调用会发生死锁问题,而在其他线程中调用则不会。

5.5.5.1 在主线程中调用

会互相等待造成死锁,代码如下:

- (void)gcdRun {
    
    NSLog(@"0----- current Thread :%@", [NSThread currentThread]);
    
    // 同步执行加主队列 在主线程环境下 卡死
    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_sync(queue, ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"3----- current Thread :%@", [NSThread currentThread]);
    });
    
    NSLog(@"E----- end Run");
}

输出如下:

2020-07-13 10:48:52.490652+0800 test19[1508:242612] 0----- current Thread :<NSThread: 0x600001765040>{number = 1, name = main}
(lldb) 

从上述可以看出

程序崩溃了,因为我们在主线程中执行gcdRun的函数,相当于把gcdRun放到了主线程的队列中,而同步执行会等待当前队列中的任务执行完毕,才会接着执行,那么我们把同步任务追加到主队列中,新增的同步任务会等待主队列中gcdRun的执行完毕后才会执行。而gcdRun任务因为在执行的过程中碰到了同步执行,所以需要等新增的同步执行结束后才能继续执行,所以两者互相等待,造成死锁。


5.5.5.1 在其他线程中调用

不会开启新线程,顺序执行任务

- (void)gcdRun {
    
    // 同步执行加主队列 在子线程环境下 顺序执行
    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        NSLog(@"0----- current Thread :%@", [NSThread currentThread]);
        
        dispatch_sync(queue, ^{
            NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
        });

        dispatch_sync(queue, ^{
            NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
        });

        dispatch_sync(queue, ^{
            NSLog(@"3----- current Thread :%@", [NSThread currentThread]);
        });
        
        NSLog(@"E----- end Run");
    });
}

输出:

2020-07-13 11:36:38.269661+0800 test19[1693:359982] 0----- current Thread :<NSThread: 0x6000021cdb00>{number = 4, name = (null)}
2020-07-13 11:36:38.270443+0800 test19[1693:359851] 1----- current Thread :<NSThread: 0x60000219e0c0>{number = 1, name = main}
2020-07-13 11:36:38.270816+0800 test19[1693:359851] 2----- current Thread :<NSThread: 0x60000219e0c0>{number = 1, name = main}
2020-07-13 11:36:38.271231+0800 test19[1693:359851] 3----- current Thread :<NSThread: 0x60000219e0c0>{number = 1, name = main}
2020-07-13 11:36:38.271592+0800 test19[1693:359982] E----- end Run

从上述可以看出

在子线程中调用同步执行+主队列并不会崩溃,没有开启新线程。

所有任务都是在0和end之间执行。且是按顺序执行的。

为什么不卡了,是因为包含1、2、3的同步执行任务块都在子线程的中被添加至主队列,主队列里被添加任务前没有正在执行的任务,所以可以执行1、2、3的同步任务,并不会卡住线程,不会造成死锁。


5.5.6 异步执行 + 主队列

只在主线程中执行任务,按顺序执行

- (void)gcdRun {
    
    NSLog(@"0----- current Thread :%@", [NSThread currentThread]);
    
    // 异步执行加主队列 不开启线程 顺序执行
    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"3----- current Thread :%@", [NSThread currentThread]);
    });
    
    NSLog(@"E----- end Run");
}

输出如下:

2020-07-13 13:34:26.452157+0800 test19[1928:419495] 0----- current Thread :<NSThread: 0x600003061100>{number = 1, name = main}
2020-07-13 13:34:26.452495+0800 test19[1928:419495] E----- end Run
2020-07-13 13:34:26.453575+0800 test19[1928:419495] 1----- current Thread :<NSThread: 0x600003061100>{number = 1, name = main}
2020-07-13 13:34:26.453896+0800 test19[1928:419495] 2----- current Thread :<NSThread: 0x600003061100>{number = 1, name = main}
2020-07-13 13:34:26.454055+0800 test19[1928:419495] 3----- current Thread :<NSThread: 0x600003061100>{number = 1, name = main}

从上述可以看出

所有任务都是在当前线程执行的,并没有开启新的线程,因为是在主队列中,所有任务都是在主线程中执行。

任务是在0和end之后才开始执行,并且是按顺序执行(因为主队列是串行队列)


5.5 GCD线程间的通讯

和上述的NSThread和NSoperation一样,将耗时操作放到子线程,操作结束之后到主线程更新UI。代码如下:

- (void)gcdCommunity {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
        //耗时操作放到子线程
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@", [NSThread currentThread]);
        }
        
        //结束后回到主线程
        dispatch_async(dispatch_get_main_queue(), ^{
            
            NSLog(@"2---%@", [NSThread currentThread]);
        });
        
    });
}

输出如下:

2020-07-13 13:38:12.455306+0800 test19[1938:431747] 1---<NSThread: 0x600001318ec0>{number = 5, name = (null)}
2020-07-13 13:38:14.456644+0800 test19[1938:431747] 1---<NSThread: 0x600001318ec0>{number = 5, name = (null)}
2020-07-13 13:38:14.456993+0800 test19[1938:431550] 2---<NSThread: 0x600001340bc0>{number = 1, name = main}

先在子线程执行完毕后,再到主线程中执行。


5.6 GCD的其他方法

5.6.1 GCD栏栅方法:dispatch_barrier_async

一堆异步执行的操作中,如果想分成两组,第一组异步执行完后,再进行第二组异步执行,中间需要一个东西分隔开来,就像栏栅一样,也就是使用dispatch_barrier_async方法。调用之后,会等待前面追加到并发队列中的任务全部执行完毕之后,再将指定任务追加到该异步队列中,然后该追加的任务执行完毕之后,这个并发队列的后续的异步任务才会执行。

代码如下:

- (void)gcdBarrir {
    
    dispatch_queue_t queue = dispatch_queue_create("com.tsh.concurrent1", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"3----- current Thread :%@", [NSThread currentThread]);
    });
    
    dispatch_barrier_async(queue, ^{
        NSLog(@"barrier----- current Thread :%@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"4----- current Thread :%@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"5----- current Thread :%@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"6----- current Thread :%@", [NSThread currentThread]);
    });
}

输出如下:

2020-07-13 13:44:20.212696+0800 test19[1938:444510] 1----- current Thread :<NSThread: 0x600001338d80>{number = 8, name = (null)}
2020-07-13 13:44:20.212878+0800 test19[1938:445014] 2----- current Thread :<NSThread: 0x60000133ba40>{number = 10, name = (null)}
2020-07-13 13:44:20.213114+0800 test19[1938:445015] 3----- current Thread :<NSThread: 0x60000133e180>{number = 11, name = (null)}
2020-07-13 13:44:20.213594+0800 test19[1938:445014] barrier----- current Thread :<NSThread: 0x60000133ba40>{number = 10, name = (null)}
2020-07-13 13:44:20.213857+0800 test19[1938:445014] 4----- current Thread :<NSThread: 0x60000133ba40>{number = 10, name = (null)}
2020-07-13 13:44:20.213906+0800 test19[1938:445015] 5----- current Thread :<NSThread: 0x60000133e180>{number = 11, name = (null)}
2020-07-13 13:44:20.213914+0800 test19[1938:444510] 6----- current Thread :<NSThread: 0x600001338d80>{number = 8, name = (null)}

可以看出前三个任务和后三个任务被分隔开来。

在日常开发中,除了上述隔开处理的需求外,使用dispatch_barrier_async,还可以作为读写锁,因为读的时候不需要关心线程的问题,只有在写的时候需要关心,因为栏栅可以对未执行的操作做阻塞,在写的时候添加到dispatch_barrier_async可以达到同步的效果。


5.6.2 GCD延时执行方法:dispatch_after

如果要延时执行某些任务,使用到的是dispatch_after方法,使用该方法并不是说指定时间才开始执行,而是在指定时间之后将任务追加到队列中,时间并不是绝对准确的,但是很有效。

代码如下:

- (void)gcdDelay {
    NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
    });
}

输出如下:

2020-07-13 13:47:57.543025+0800 test19[1938:431550] 1----- current Thread :<NSThread: 0x600001340bc0>{number = 1, name = main}
2020-07-13 13:47:58.543693+0800 test19[1938:431550] 2----- current Thread :<NSThread: 0x600001340bc0>{number = 1, name = main}

可以看出两条输出之间相差了大约1秒的时间。


5.6.3 GCD只执行一次的方法:dispatch_once

如果在开发中创建单例或者某些代码只需要执行一次,使用到的是dispatch_once方法,即使在多线程的环境下,也可以保证线程安全。

- (void)gcdOnce {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    });
}

其中onceToken可以设置成属性,然后赋值为0之后,可以再次进行一次性代码的调用。


5.6.4 GCD快速迭代方法:dispatch_apply

如果需要进行快速遍历,并且加上并发队列进行异步执行,可以采用dispatch_apply方法在多线程中同时处理任务。

dispatch_apply不管在并发还是串行队列中,都是会等待全部任务执行完毕才会继续往下走。

代码如下:

- (void)gcdFastApply {
    
    NSLog(@"apply begin");
    dispatch_apply(6, dispatch_get_global_queue(0, 0), ^(size_t index) {
        NSLog(@"%zu----- current Thread :%@",index, [NSThread currentThread]);
    });
    NSLog(@"apply end");
}

输出如下:

2020-07-13 13:53:55.396380+0800 test19[1938:431550] apply begin
2020-07-13 13:53:55.396734+0800 test19[1938:431550] 0----- current Thread :<NSThread: 0x600001340bc0>{number = 1, name = main}
2020-07-13 13:53:55.396923+0800 test19[1938:444510] 1----- current Thread :<NSThread: 0x600001338d80>{number = 8, name = (null)}
2020-07-13 13:53:55.397017+0800 test19[1938:431550] 2----- current Thread :<NSThread: 0x600001340bc0>{number = 1, name = main}
2020-07-13 13:53:55.397052+0800 test19[1938:471666] 3----- current Thread :<NSThread: 0x6000013c4540>{number = 12, name = (null)}
2020-07-13 13:53:55.397108+0800 test19[1938:444510] 4----- current Thread :<NSThread: 0x600001338d80>{number = 8, name = (null)}
2020-07-13 13:53:55.397143+0800 test19[1938:471665] 5----- current Thread :<NSThread: 0x6000013c0240>{number = 13, name = (null)}
2020-07-13 13:53:55.397419+0800 test19[1938:431550] apply end

可以看出,在遍历的任务里面是在不同的线程中执行的,但是最后遍历结束之后才会继续往下走。


5.6.5 GCD队列组:dispatch_group

如果需要分别执行某些异步耗时的任务,然后当这些任务都结束时再回到主线程执行任务,这时候可以采用dispatch_group方法来进行处理。

首先需要将任务加入队列组,使用dispatch_group_async或者使用dispatch_group_enter和dispatch_group_leave的组合。

然后使用dispatch_group_notify或者dispatch_group_wait来对任务的结果进行处理。

代码如下:

先通过dispatch_group_async加入队列组,然后通过dispatch_group_notify接收结束的通知。

- (void)gcdGroup {
    NSLog(@"group begin thread:%@", [NSThread currentThread]);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
    });
    
    // 使用Notify
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"3----- current Thread :%@", [NSThread currentThread]);
        NSLog(@"group end");
    });
}

输出如下:

2020-07-13 13:59:36.658861+0800 test19[1995:488938] group begin thread:<NSThread: 0x600000711080>{number = 1, name = main}
2020-07-13 13:59:36.659331+0800 test19[1995:489117] 2----- current Thread :<NSThread: 0x60000074af40>{number = 3, name = (null)}
2020-07-13 13:59:36.659339+0800 test19[1995:489116] 1----- current Thread :<NSThread: 0x600000765b80>{number = 6, name = (null)}
2020-07-13 13:59:36.660207+0800 test19[1995:488938] 3----- current Thread :<NSThread: 0x600000711080>{number = 1, name = main}
2020-07-13 13:59:36.660493+0800 test19[1995:488938] group end

可以看出在前面两个任务都执行完毕之后,才执行的回到主线程的处理。


接下来再通过dispatch_group_wait来对结果进行等待

- (void)gcdGroup {
    NSLog(@"group begin thread:%@", [NSThread currentThread]);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
    });

    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
    });
    
    //使用wait
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    NSLog(@"group end");
}

输出如下:

2020-07-13 14:01:40.286656+0800 test19[2016:496550] group begin thread:<NSThread: 0x600001fa6140>{number = 1, name = main}
2020-07-13 14:01:40.287057+0800 test19[2016:496696] 2----- current Thread :<NSThread: 0x600001ff7fc0>{number = 5, name = (null)}
2020-07-13 14:01:40.287056+0800 test19[2016:496697] 1----- current Thread :<NSThread: 0x600001fd6200>{number = 7, name = (null)}
2020-07-13 14:01:40.287336+0800 test19[2016:496550] group end

同样的,也是在前面两个任务都执行完毕之后,才往后面走。需要注意的是,dispatch_group_wait会阻塞当前线程。


最后通过使用dispatch_group_enter和dispatch_group_leave的组合来进行调用,适用于一些异步的回调中使用,例如网络请求回调的场景中使用。

代码如下:

- (void)gcdGroup {
    NSLog(@"group begin thread:%@", [NSThread currentThread]);
    dispatch_group_t group = dispatch_group_create();
    
    // 使用enter 和 leave
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"2----- current Thread :%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"3----- current Thread :%@", [NSThread currentThread]);
        NSLog(@"group end");
    });
    
}

输出如下:

2020-07-13 14:04:44.620372+0800 test19[2043:506703] group begin thread:<NSThread: 0x60000120a100>{number = 1, name = main}
2020-07-13 14:04:44.620827+0800 test19[2043:507253] 1----- current Thread :<NSThread: 0x600001288540>{number = 9, name = (null)}
2020-07-13 14:04:44.620827+0800 test19[2043:506865] 2----- current Thread :<NSThread: 0x600001258e40>{number = 4, name = (null)}
2020-07-13 14:04:44.621661+0800 test19[2043:506703] 3----- current Thread :<NSThread: 0x60000120a100>{number = 1, name = main}
2020-07-13 14:04:44.621904+0800 test19[2043:506703] group end

效果和使用了dispatch_group_async一致。


5.6.6 GCD定时器:dispatch_source_t Timer

Dispatch Source Timer 是GCD中配合GCD队列使用的定时器,相对于NSTimer,不用依赖于RunLoop,所以相对更准时些。

使用方式如下:

@property (nonatomic, strong) dispatch_source_t timerSource;

self.timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_source_set_timer(self.timerSource, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, NSEC_PER_SEC);
dispatch_source_set_event_handler(self.timerSource, ^{
    NSLog(@"123");
});
dispatch_resume(self.self.timerSource);

// 退出时停止计时器
dispatch_source_cancel(self.timerSource);

其中除了使用cancel销毁计时器外。

resume还配对着suspend使用,对应着运行和暂停。

但调用了suspend后,当前正在执行的block是不会立即停止的,到下一次执行前会暂停,同时计时器也是没有销毁的。同时需要注意循环引用的问题。


5.6.7 GCD信号量:dispatch_semaphore

GCD中的信号量是持有计数的信号,类似于通过告诉收费站的栏杆,有时候限制只能1个通过,有时候限制2个,或者有时候都不开放。在dispatch_semaphore中,计数小于0时等待,不可通过。计数为0或者大于0时,可以通过,同时计数减1并且不等待。其中有三个方法:

  • dispatch_semaphore_create: 创建一个Semaphore初始化信号的总量
  • dispatch_semaphore_signal: 发送一个信号,让信号总量加1
  • dispatch_semaphore_wait: 使总量信号减1,信号总量小于0时就会一直等待,阻塞当前线程,否则就可以正常执行。

使用信号量,可以实现:

  • 将异步代码转成同步执行。
  • 保证线程安全,为线程加锁。

5.6.7.1 实现异步执行任务转成同步执行

代码如下:

- (void)gcdSemaphore {
    
    // 异步转同步
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    __block int someValue = 0;

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"1----- current Thread :%@", [NSThread currentThread]);
        someValue = 100;
        dispatch_semaphore_signal(semaphore);
    });

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    NSLog(@"value = %d", someValue);
    
}

输出如下:

2020-07-13 14:11:46.360160+0800 test19[2078:527513] 1----- current Thread :<NSThread: 0x600000a1e8c0>{number = 4, name = (null)}
2020-07-13 14:11:46.360473+0800 test19[2078:527206] value = 100

可以看出,最后输出的value是异步执行任务内的值。说明value输出是在赋值之后,实现了异步任务回到同步任务上。


5.6.7.2 实现线程安全和线程加锁

和上诉NSThread和NSOperation一样,使用火车售票的例子进行说明。

首先先看不采用semaphore的情况:

- (void)gcdSemaphore {
    dispatch_queue_t queue1 = dispatch_queue_create("com.tsh.serial1", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("com.tsh.serial2", DISPATCH_QUEUE_SERIAL);

    self.ticketCount = 50;

    dispatch_async(queue1, ^{
        [self buyTicket];
    });

    dispatch_async(queue2, ^{
        [self buyTicket];
    });
}

- (void)buyTicket {
    while (1) {
        if (self.ticketCount > 0) {
            self.ticketCount--;
            [NSThread sleepForTimeInterval:0.1
             ];
            NSLog(@"剩余票数为:%d  thread:%@", self.ticketCount, [NSThread currentThread]);
        }
        if (self.ticketCount <= 0) {
            NSLog(@"票卖完了");
            break;
        }
    }
}

输出如下:

2020-07-13 14:16:33.249254+0800 test19[2100:540089] 剩余票数为:48  thread:<NSThread: 0x6000026ee340>{number = 4, name = (null)}
2020-07-13 14:16:33.249274+0800 test19[2100:543090] 剩余票数为:48  thread:<NSThread: 0x600002650a00>{number = 7, name = (null)}
2020-07-13 14:16:33.352636+0800 test19[2100:543090] 剩余票数为:47  thread:<NSThread: 0x600002650a00>{number = 7, name = (null)}
2020-07-13 14:16:33.352684+0800 test19[2100:540089] 剩余票数为:47  thread:<NSThread: 0x6000026ee340>{number = 4, name = (null)}
......
2020-07-13 14:16:35.816891+0800 test19[2100:543090] 剩余票数为:1  thread:<NSThread: 0x600002650a00>{number = 7, name = (null)}
2020-07-13 14:16:35.817307+0800 test19[2100:543090] 票卖完了
2020-07-13 14:16:35.917903+0800 test19[2100:540089] 剩余票数为:0  thread:<NSThread: 0x6000026ee340>{number = 4, name = (null)}
2020-07-13 14:16:35.918080+0800 test19[2100:540089] 票卖完了

从打印上看是不符合我们预期的。


接着使用semaphore进行加锁处理,代码如下:

- (void)gcdSemaphore {
    // 线程加锁
    self.semaphore = dispatch_semaphore_create(1);

    dispatch_queue_t queue1 = dispatch_queue_create("com.tsh.serial1", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("com.tsh.serial2", DISPATCH_QUEUE_SERIAL);

    self.ticketCount = 50;

    dispatch_async(queue1, ^{
        [self buyTicket];
    });

    dispatch_async(queue2, ^{
        [self buyTicket];
    });
}

- (void)buyTicket {
    while (1) {
        
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        
        if (self.ticketCount > 0) {
            self.ticketCount--;
            [NSThread sleepForTimeInterval:0.1
             ];
            NSLog(@"剩余票数为:%d  thread:%@", self.ticketCount, [NSThread currentThread]);
        }
        
        dispatch_semaphore_signal(self.semaphore);
        
        if (self.ticketCount <= 0) {
            NSLog(@"票卖完了");
            break;
        }
    }
}

输出如下:

2020-07-13 14:18:27.538624+0800 test19[2121:550108] 剩余票数为:49  thread:<NSThread: 0x6000012f43c0>{number = 5, name = (null)}
2020-07-13 14:18:27.639761+0800 test19[2121:550114] 剩余票数为:48  thread:<NSThread: 0x6000012ed400>{number = 6, name = (null)}
2020-07-13 14:18:27.742686+0800 test19[2121:550108] 剩余票数为:47  thread:<NSThread: 0x6000012f43c0>{number = 5, name = (null)}
2020-07-13 14:18:27.846622+0800 test19[2121:550114] 剩余票数为:46  thread:<NSThread: 0x6000012ed400>{number = 6, name = (null)}
......
2020-07-13 14:18:32.502198+0800 test19[2121:550108] 剩余票数为:1  thread:<NSThread: 0x6000012f43c0>{number = 5, name = (null)}
2020-07-13 14:18:32.605474+0800 test19[2121:550114] 剩余票数为:0  thread:<NSThread: 0x6000012ed400>{number = 6, name = (null)}
2020-07-13 14:18:32.605836+0800 test19[2121:550114] 票卖完了
2020-07-13 14:18:32.605836+0800 test19[2121:550108] 票卖完了

从打印上看是正确的剩余票数,解决了多个线程同步的问题。