前言:这可能是史上最全面的一篇iOS 多线程博客了(王婆卖瓜一番),从多线程的基本概念,进程的概念,引出iOS中的四种多线程方案pthread、NSThread、NSOperation和GCD,每一部分都有详细的代码和解释说明;在GCD中,引出同步、异步、串行队列(包括主队列)和并发队列概念,并对他们的六种组合进行详细的代码验证和说明,把这些概念安排的明明白白,然后详细的说明了GCD常见的其他用法;最后,对iOS中线程安全的方案进行全方面的介绍说明并且配上示例代码。好了,小编累的吐血去了...
一、多线程概念
1、多线程概念:
一个进程中可以开启多条线程,每条线程可以并行执行不同的任务。多线程可以充分的利用多个CPU同时处理任务,提高程序的执行效率。
2、进程概念:
进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的。而线程是应用程序中一条任务的执行路径。
3、进程和线程的关系:
1)一个进程可以包含多个线程;
2)一个进程中的所有任务都是在线程中执行;
4、iOS多线程实现方案:
1)pthread:纯C语言API,是一套通用的多线程API,适用于Linux、Unix、Windows等系统,线程生命周期由程序员管理。在iOS实际开发中,使用较少。
2)NSThread:使用更加面向对象,并可直接操作线程对象,线程生命周期由程序员管理,项目开发中使用较多;
3)NSOperation:基于GCD,使用更加面向对象,线程生命周期系统自动管理,使用较多;
4)GCD:一套改进的C语言多线程API,能充分利用设备的多核优势,线程生命周期系统自动管理,使用最多;
二、pThread
纯C语言API,使用较为麻烦。
#import
- (void)viewDidLoad {
[super viewDidLoad];
[self pthread];
}
//开启新的线程执行run方法
- (void)pthread {
pthread_t thread;
pthread_create(&thread, NULL, run, NULL);
}
void * run(void *param){
NSLog(@"Thread = %@", [NSThread currentThread]);
return NULL;
}
三、NSThread
1、NSThread的创建
一个NSThread对象就代表一个线程,有三种方式创建:
1)创建线程后需要start启动线程;
2)创建线程后自动启动线程;
3)隐式创建并启动线程;
- (void)viewDidLoad {
[super viewDidLoad];
/*
创建线程,线程对象(局部变量)系统会自己加持,在任务执行完之前不会被销毁
当线程任务执行完毕,线程自动销毁
*/
// 1、使用start启动线程
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"thread1"];
thread1.name = @"thread1";
[thread1 start];
// 2、自动启动线程
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"thread2"];
// 3、隐式启动线程
[self performSelectorInBackground:@selector(run:) withObject:@"thread3"];
}
- (void)run:(NSString *)threadStr {
if ([threadStr isEqualToString:@"thread1"]) {
NSLog(@"thread1 = %@", [NSThread currentThread]);
} else if ([threadStr isEqualToString:@"thread2"]) {
NSLog(@"thread2 = %@", [NSThread currentThread]);
} else if ([threadStr isEqualToString:@"thread3"]) {
NSLog(@"thread3 = %@", [NSThread currentThread]);
}
}
运行程序,打印结果:
thread1 = {number = 6, name = thread1}
thread2 = {number = 7, name = (null)}
thread3 = {number = 8, name = (null)}
2、NSThread常用用法
//获得当前线程
NSThread *current = [NSThread currentThread];
+ (NSThread *)mainThread; // 获得主线程
- (BOOL)isMainThread; // 是否为主线程
+ (BOOL)isMainThread; // 是否为主线程
//线程的名字,适用于第一种方式创建的线程(创建的时候返回NSThread的对象)
- (void)setName:(NSString *)n;
- (NSString *)name;
3、NSThread的线程通信
在开发中,我们经常会在子线程进行耗时操作,操作结束后再回到主线程去刷新 UI。这就涉及到了子线程和主线程之间的通信,常用两个API如下:
/**
回到主线程执行任务,RunLoop模式默认是kCFRunLoopCommonModes
@param aSelector:选择器(方法)
@param arg:传递的参数
@param wait:是否等待任务执行完毕
*/
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
/**
在指定线程中执行任务,RunLoop模式默认是kCFRunLoopCommonModes
@param aSelector:选择器(方法)
@param thr:指定的线程
@param arg:传递的参数
@param wait:是否等待任务执行完毕
*/
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
模拟子线程下载,主线程刷新UI代码:
- (void)viewDidLoad {
[super viewDidLoad];
// 创建新线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(runTestThread) object:nil];
[thread start];
// 注意:这里waitUntilDone只能是NO,不然程序闪退
[self performSelector:@selector(runTestThread) onThread:thread withObject:nil waitUntilDone:NO];
}
// 子线程,执行耗时操作
- (void)runTestThread {
NSLog(@"runTestThread = %@", [NSThread currentThread]);
sleep(3);
// waitUntilDone如果是YES,那么会阻塞当前线程,waitUntilDone会最后打印
[self performSelectorOnMainThread:@selector(runMainThread) withObject:nil waitUntilDone:NO];
NSLog(@"waitUntilDone...");
}
// 主线程,刷新UI
- (void)runMainThread {
NSLog(@"runMainThread = %@", [NSThread currentThread]);
}
程序运行打印结果:
runTestThread = {number = 6, name = (null)}
waitUntilDone... // waitUntilDone如果是YES,那么会阻塞当前线程,waitUntilDone会最后打印
runMainThread = {number = 1, name = main}
四、NSOperation
NSOperation和NSOperationQueue是对GCD的一层封装,NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类NSInvocationOperation和NSBlockOperation。
1、NSInvocationOperation
NSInvocationOperation代码:
- (void)viewDidLoad {
[super viewDidLoad];
/**
默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作
只有将NSOperation放到一个NSOperationQueue中,才会异步执行操作
*/
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
[op start];
}
- (void)run {
NSLog(@"mainThread = %@", [NSThread currentThread]);
}
运行结果:
// 说明还是当前的线程,并没有开启新线程
mainThread = {number = 1, name = main}
2、NSBlockOperation
NSBlockOperation代码:
- (void)viewDidLoad {
[super viewDidLoad];
// 通过一个block创建NSBlockOperation实例
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
/**
如果NSBlockOperation没有再添加block,即只有一个操作数,那么这里肯定是主线程
如果NSBlockOperation封装的操作数 > 1,就会异步执行操作,那么实际测试,这里不一定是主线程
*/
NSLog(@"Thread1 = %@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"Thread3 = %@", [NSThread currentThread]);
}];
// 开始
[op start];
}
运行结果:Thread1、Thread2和Thread3当中肯定会有一个是主线程。
Thread1 = {number = 4, name = (null)} //不一定是主线程
Thread2 = {number = 6, name = (null)}
Thread3 = {number = 1, name = main}
3、NSOperationQueue
NSOperation可以调用start方法来执行任务,但默认是同步执行的,如果将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行操作。
1)NSInvocationOperation与NSOperationQueue组合:
- (void)viewDidLoad {
[super viewDidLoad];
// 1、创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2、创建NSInvocationOperation
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run1) object:nil];
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run2) object:nil];
// 3、添加任务到队列中
[queue addOperation:op1]; // [op1 start]
[queue addOperation:op2]; // [op2 start]
}
- (void)run1 {
NSLog(@"Thread1 = %@", [NSThread currentThread]);
}
- (void)run2 {
NSLog(@"Thread2 = %@", [NSThread currentThread]);
}
运行结果:Thread1和Thread2都是在子线程,而不是在主线程,是异步的。
Thread1 = {number = 5, name = (null)}
Thread2 = {number = 6, name = (null)}
2)NSBlockOperation与NSOperationQueue组合:
- (void)viewDidLoad {
[super viewDidLoad];
// 1、创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2、创建NSBlockOperation
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"Thread1 = %@", [NSThread currentThread]);
}];
[op1 addExecutionBlock:^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
}];
[op1 addExecutionBlock:^{
NSLog(@"Thread3 = %@", [NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"Thread4 = %@", [NSThread currentThread]);
}];
// 3、添加任务到队列中
[queue addOperation:op1];
[queue addOperation:op2];
}
运行结果:Thread1、Thread2、Thread3、Thread4都是子线程,所以添加到队列之后,是异步的。
Thread4 = {number = 6, name = (null)}
Thread2 = {number = 5, name = (null)}
Thread3 = {number = 7, name = (null)}
Thread1 = {number = 4, name = (null)}
4、控制NSOperationQueue是串行队列还是并发队列
可以通过设置maxConcurrentOperationCount的值来选择串行队列还是并发队列。
- (void)viewDidLoad {
[super viewDidLoad];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
/**
maxCount等于-1:默认值,也就是不限制最大并发数,添加的operation都是异步的
maxCount等于0:不执行operation
maxCount等于1:在子线程同步执行operation,也就是串行队列
maxCount大于1:在指定数量的线程内异步处理operation
maxCount为负数,且不等于-1:程序抛出异常,count cannot be negative
*/
queue.maxConcurrentOperationCount = -1;
// 添加操作
[queue addOperationWithBlock:^{
NSLog(@"Thread1 = %@", [NSThread currentThread]);
// 切换到主线程
NSOperationQueue *main = [NSOperationQueue mainQueue];
[main addOperationWithBlock:^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
}];
}];
[queue addOperationWithBlock:^{
NSLog(@"Thread3 = %@", [NSThread currentThread]);
}];
}
运行结果:
Thread1 = {number = 4, name = (null)}
Thread3 = {number = 6, name = (null)}
Thread2 = {number = 1, name = main}
5、NSOperation之间可以设置依赖来保证执行顺序
比如一定要让操作A执行完后,才能执行操作B,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"Thread1 = %@", [NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
}];
[op2 addExecutionBlock:^{
NSLog(@"Thread3 = %@", [NSThread currentThread]);
}];
/**
1、[op1 addDependency:op2]:op1依赖于op2执行完成
2、如果同时设置op1依赖于op2,op2依赖于op1,会造成死锁,不会执行任务了
3、任务可以跨队列依赖,在不同队列里面的任务也可以相互依赖
*/
[op1 addDependency:op2];
[queue addOperation:op1];
[queue addOperation:op2];
}
运行结果:先执行op2(Thread2、Thread3),后执行op1(Thread1)。
Thread3 = {number = 7, name = (null)}
Thread2 = {number = 6, name = (null)}
Thread1 = {number = 6, name = (null)}
五、GCD
Grand Central Dispatch (GCD)是Apple开发的一个多核编程的较新的解决方法,它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统,它是一个在线程池模式的基础上执行的并行任务,GCD是一个替代诸如NSThread等技术的很高效和强大的技术。
GCD 会自动利用更多的 CPU 内核(比如双核、四核),GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程),程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
GCD两个核心概念,任务和队列,任务包括(同步执行任务、异步执行任务),队列包括(串行队列、并发队列),队列采用 FIFO(First In First Out)的原则,即先进先出原则。
1、同步执行、异步执行
同步执行与异步执行的区别:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
1)同步执行(sync):同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。只能在当前线程中执行任务,不具备开启新线程的能力。
2)异步执行(async):异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。可以在新的线程中执行任务,具备开启新线程的能力。
注意:异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。
2、串行队列、并发队列
串行队列和并发队列的区别:执行顺序不同,以及开启线程数不同。
1)串行队列(Serial Dispatch Queue):让任务一个接着一个地执行(最多创建一个线程)。dispatch_get_main_queue() 主队列就是一个串行队列。
2)并发队列(Concurrent Dispatch Queue):可以让多个任务并发(同时)执行(可以开启多个线程)。dispatch_get_global_queue(0, 0) 全局队列就是并发队列。
注意:并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。
3、经典各种组合模式
本来同步、异步、串行、并发有四种组合,但是当前代码默认放在主队列中,全局并发队列可以作为普通并发队列来使用,所以主队列很有必要专门来研究一下,所以我们就有六种组合模式了。
1)同步执行 + 串行队列:没有开启新线程,串行执行任务。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t serialQueue = dispatch_queue_create("SerialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
NSLog(@"Thread1 = %@", [NSThread currentThread]);
});
dispatch_sync(serialQueue, ^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
});
dispatch_sync(serialQueue, ^{
NSLog(@"Thread3 = %@", [NSThread currentThread]);
});
}
运行结果:线程还是main主线程,没有开启新线程,Thread1、Thread2、Thread3按顺序执行。
Thread1 = {number = 1, name = main}
Thread2 = {number = 1, name = main}
Thread3 = {number = 1, name = main}
2)同步执行 + 并发队列:没有开启新线程,串行执行任务。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t concurrentQueue = dispatch_queue_create("ConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(concurrentQueue, ^{
NSLog(@"Thread1 = %@", [NSThread currentThread]);
});
dispatch_sync(concurrentQueue, ^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
});
dispatch_sync(concurrentQueue, ^{
NSLog(@"Thread3 = %@", [NSThread currentThread]);
});
}
运行结果:线程还是main主线程,没有开启新线程,Thread1、Thread2、Thread3按顺序执行。
Thread1 = {number = 1, name = main}
Thread2 = {number = 1, name = main}
Thread3 = {number = 1, name = main}
3)异步执行 + 串行队列:有开启新线程(1条),串行执行任务。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t serialQueue = dispatch_queue_create("SerialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
NSLog(@"Thread1 = %@", [NSThread currentThread]);
});
dispatch_async(serialQueue, ^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
});
dispatch_async(serialQueue, ^{
NSLog(@"Thread3 = %@", [NSThread currentThread]);
});
}
运行结果:有开启一条新线程,Thread1、Thread2、Thread3按顺序执行。
Thread1 = {number = 6, name = (null)}
Thread2 = {number = 6, name = (null)}
Thread3 = {number = 6, name = (null)}
4)异步执行 + 并发队列:有开启新线程(多条),并发执行任务。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t concurrentQueue = dispatch_queue_create("ConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
NSLog(@"Thread1 = %@", [NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
NSLog(@"Thread3 = %@", [NSThread currentThread]);
});
}
运行结果:有开启多条新线程,Thread1、Thread2、Thread3是并发执行的。
Thread2 = {number = 7, name = (null)}
Thread1 = {number = 6, name = (null)}
Thread3 = {number = 5, name = (null)}
5)同步执行+主队列:死锁卡住不执行。其实如果在串行队列中嵌套同步使用串行队列,也会发生死锁,原理和这个类似。所以项目中数据库处理FMDataQueue嵌套使用时,需要注意死锁问题。
如果不是在主线程执行同步执行+主队列呢,那么不会发生死锁(读者可以自己代码测试验证)。
- (void)viewDidLoad {
[super viewDidLoad];
/**
dispatch_queue_t serialQueue = dispatch_queue_create("SerialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{ //异步串行,创建一条新线程
NSLog(@"Thread1 = %@", [NSThread currentThread]);
dispatch_sync(serialQueue, ^{ //嵌套同步串行,发生死锁
NSLog(@"Thread2 = %@", [NSThread currentThread]);
});
});
*/
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
NSLog(@"Thread1 = %@", [NSThread currentThread]);
});
dispatch_sync(mainQueue, ^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
});
dispatch_sync(mainQueue, ^{
NSLog(@"Thread3 = %@", [NSThread currentThread]);
});
}
运行结果:程序死锁崩溃,原因是默认viewDidLoad被添加到主队列中(先运行完viewDidLoad,后运行添加的任务),然后又同步添加Thread1任务到主队列中(先运行Thread1任务,后运行viewDidLoad任务),造成任务相互等待卡死,程序死锁崩溃。
6)异步执行+主队列:没有开启新线程,串行执行任务。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^{
NSLog(@"Thread1 = %@", [NSThread currentThread]);
});
dispatch_async(mainQueue, ^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
});
dispatch_async(mainQueue, ^{
NSLog(@"Thread3 = %@", [NSThread currentThread]);
});
}
运行结果:线程还是main主线程,没有开启新线程,Thread1、Thread2、Thread3按顺序执行。
Thread1 = {number = 1, name = main}
Thread2 = {number = 1, name = main}
Thread3 = {number = 1, name = main}
4、GCD线程间的通信
在项目开发中,一般耗时的操作在别的线程处理,然后在主线程刷新UI,GCD线程间的通信如下:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(2); //模拟耗时操作
NSLog(@"Thread1 = %@", [NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
});
});
}
运行结果:在子线程执行耗时操作,再回到主线程刷新UI。
Thread1 = {number = 5, name = (null)}
Thread2 = {number = 1, name = main}
5、GCD其他的常见用法
1)栅栏方法(dispatch_barrier_async)
我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作,这就需要用到dispatch_barrier_async 方法在两个操作组间形成栅栏。NSOperation的 addDependency 也是这个效果。
- (void)viewDidLoad {
[super viewDidLoad];
// 创建并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("ConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
sleep(1); //模拟耗时操作
NSLog(@"Thread1 = %@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
sleep(1);
NSLog(@"Thread2 = %@",[NSThread currentThread]);
});
// 添加栅栏
dispatch_barrier_async(concurrentQueue, ^{
sleep(1);
NSLog(@"Thread3 = %@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
sleep(1);
NSLog(@"Thread4 = %@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{
sleep(1);
NSLog(@"Thread5 = %@",[NSThread currentThread]);
});
}
运行结果:先执行第一组任务(Thread1、Thread2),然后再执行栅栏的第二组任务(Thread3、Thread4、Thread5)。
Thread2 = {number = 4, name = (null)}
Thread1 = {number = 7, name = (null)}
Thread3 = {number = 4, name = (null)}
Thread5 = {number = 6, name = (null)}
Thread4 = {number = 4, name = (null)}
2)延时方法(dispatch_after)
项目中我们有时需要几秒之后再执行某个方法,那么可以使用GCD的延时方法,而不用创建一个定时器来处理。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"Thread1 = %@", [NSThread currentThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2.0 秒后异步追加任务代码到主队列,并开始执行,如果主线程被阻塞,时间上不会非常准确
NSLog(@"Thread2 = %@", [NSThread currentThread]);
});
sleep(5); //阻塞主线程
NSLog(@"Thread3 = %@", [NSThread currentThread]);
}
运行结果:先执行Thread1,5s之后执行Thread3,当前主队列执行完毕,然后执行dispatch_after添加的任务Thread2。至于为什么Thread3、Thread2时间接近,而不是相差2s,因为执行完Thread1的2s之后,Thread2添加到主队列,正在等待主队列执行完毕。
2019-10-24 10:03:05.638019 Thread1 = {number = 1, name = main}
2019-10-24 10:03:10.639501 Thread3 = {number = 1, name = main}
2019-10-24 10:03:10.658852 Thread2 = {number = 1, name = main}
假如注释掉 sleep(5),那么运行结果:主队列没被阻塞,Thread2任务基本2s之后就会执行。
2019-10-24 10:10:55.653691 Thread1 = {number = 1, name = main}
2019-10-24 10:10:55.653898 Thread3 = {number = 1, name = main}
2019-10-24 10:10:57.654041 Thread2 = {number = 1, name = main}
3)执行一次(dispatch_once)
项目开发中,我们经常使用单例模式,那么就对 dispatch_once 很熟悉,代码只运行一次。并且即使在多线程的环境下,dispatch_once 也可以保证线程安全。
- (void)viewDidLoad {
[super viewDidLoad];
[self executeOnce]; //先执行一次
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"Thread1 = %@", [NSThread currentThread]);
[self executeOnce];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"Thread2 = %@", [NSThread currentThread]);
[self executeOnce];
});
}
- (void)executeOnce {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只会执行一次
NSLog(@"Thread3 = %@", [NSThread currentThread]);
});
}
运行结果:Thread3 只被执行一次,并且是线程安全的。
Thread3 = {number = 1, name = main}
Thread1 = {number = 5, name = (null)}
Thread2 = {number = 3, name = (null)}
4)快速迭代方法(dispatch_apply)
通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的方法 dispatch_apply。dispatch_apply 按照指定的次数将指定的任务追加到指定的队列中,并等待全部任务执行结束。如果是在串行队列中使用dispatch_apply,那么就和 for 循环一样,按顺序同步执行,所以实际使用的时候一般用并发队列。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(0, 0); //全局并发队列
NSLog(@"dispatch_apply begin");
dispatch_apply(5, queue, ^(size_t index) {
sleep(1);
NSLog(@"Thread_%ld = %@", index, [NSThread currentThread]);
});
NSLog(@"dispatch_apply end");
}
运行结果:先异步执行完所有的任务,最后执行dispatch_apply end。
dispatch_apply begin
Thread_0 = {number = 4, name = (null)}
Thread_2 = {number = 6, name = (null)}
Thread_1 = {number = 5, name = (null)}
Thread_3 = {number = 1, name = main}
Thread_4 = {number = 4, name = (null)}
dispatch_apply end
5)队列组(dispatch_group)
有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务,这时候我们可以用到 GCD 的队列组。项目中常见使用场景有:请求多个接口数据返回后,再统一进行刷新;下载完多张图片后,再统一进行合并绘制等。
调用队列组的 dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enter、dispatch_group_leave 组合来实现 dispatch_group_async。
调用队列组的 dispatch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程)。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, globalQueue, ^{
sleep(1);
NSLog(@"Thread1 = %@",[NSThread currentThread]);
});
dispatch_group_async(group, globalQueue, ^{
sleep(1);
NSLog(@"Thread2 = %@",[NSThread currentThread]);
});
/**
通过enter、leave实现
dispatch_group_enter:标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1
dispatch_group_leave:标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1
当 group 中未执行任务数为0的时候,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 中的任务
*/
dispatch_group_enter(group);
dispatch_async(globalQueue, ^{
sleep(1);
NSLog(@"Thread3 = %@",[NSThread currentThread]);
dispatch_group_leave(group);
});
// 通知
dispatch_group_notify(group, mainQueue, ^{
// 等前面的异步任务1、任务2都执行完毕后,回到主线程执行下边任务
sleep(1);
NSLog(@"Thread4 = %@",[NSThread currentThread]);
});
NSLog(@"dispatch_group_wait 之前");
// 设置DISPATCH_TIME_FOREVER会一直等待group结束,会阻塞当前线程,如果设置为DISPATCH_TIME_NOW就不会等待
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"group 执行完毕");
}
运行结果:先异步执行group任务(Thread1、Thread2、Thread3),group执行完毕后,再执行nofity的任务。
dispatch_group_wait 之前
Thread2 = {number = 6, name = (null)}
Thread3 = {number = 4, name = (null)}
Thread1 = {number = 7, name = (null)}
group 执行完毕
Thread4 = {number = 1, name = main}
6)信号量(dispatch_semaphore)
项目开发中,有时候有这样的需求:异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。比如说:AFNetworking 中 AFURLSessionManager.m 里面的 tasksForKeyPath: 方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks。
GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数小于 0 时等待,不可通过。计数为 0 或大于 0 时,不等待,可通过。信号量主要用于:
a、保持线程同步,将异步执行任务转换为同步执行任务;
b、保证线程安全,为线程加锁;
// AFNetworking 部分源码
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}
我们自己用信号量实现线程同步:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"dispatch_semaphore begin");
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
/**
创建信号量
小于0:程序闪退
等于0:正常使用,会同步执行代码
大于0:不会同步执行
*/
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
/**
dispatch_semaphore_signal:发送一个信号,让信号总量加 1
dispatch_semaphore_wait:可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。
*/
dispatch_async(globalQueue, ^{
sleep(1);
NSLog(@"Thread1 = %@", [NSThread currentThread]);
dispatch_semaphore_signal(semaphore); //信号量 +1
});
NSLog(@"dispatch_semaphore_wait 之前");
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //信号量 -1
NSLog(@"dispatch_semaphore end");
}
运行结果:semaphore初始创建时计数为 0,异步将Thread1任务添加到全局并发队列,不做等待,接着执行 dispatch_semaphore_wait 方法,semaphore 减 1,此时 semaphore == -1,当前线程进入等待状态。
然后,异步任务 1 开始执行。任务 1 执行到 dispatch_semaphore_signal 之后,总信号量加 1,此时 semaphore == 0,正在被阻塞的线程(主线程)恢复继续执行,最后打印end。
dispatch_semaphore begin
dispatch_semaphore_wait 之前
Thread1 = {number = 4, name = (null)}
dispatch_semaphore end
我们自己用信号量实现 semaphore 加锁:
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 10;
/**
注意:这里参数是1
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
*/
self.semaphoreLock = dispatch_semaphore_create(1); //
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_async(globalQueue, ^{ //第一条线程
[self sellTickets];
});
dispatch_async(globalQueue, ^{ //第二条线程
[self sellTickets];
});
dispatch_async(globalQueue, ^{ //第三条线程
[self sellTickets];
});
}
- (void)sellTickets {
while (1) { //一直运行,直到break退出
// 等待信号量,类似于加锁,此时信号量减一,如果信号量减到小于0,那么就等待一个新的信号量发送
dispatch_semaphore_wait(self.semaphoreLock, DISPATCH_TIME_FOREVER);
if (self.ticketCount > 0) { //如果还有票,继续售卖
self.ticketCount --;
NSLog(@"剩余票数 = %ld, 线程 = %@", self.ticketCount, [NSThread currentThread]);
[NSThread sleepForTimeInterval:0.2]; //模拟耗时操作
} else { //如果已卖完,关闭售票窗口
NSLog(@"火车票售完");
// 发送一个信号量,类似于解锁
dispatch_semaphore_signal(self.semaphoreLock);
break;
}
// 发送一个信号量,类似于解锁
dispatch_semaphore_signal(self.semaphoreLock);
}
}
运行结果:ticketCount 按照顺序依次递减1,“火车票售完”打印三次是因为有三条线程运行。
分析过程:首先三条线程都并发执行sellTickets方法,最快的一条线程首先执行dispatch_semaphore_wait,信号量减一,此时信号量为0,该条线程继续执行wait之后的代码,而其他较慢的两条线程进行等待新的信号量出现,较快的一条线程卖票之后,发送一个信号量,那么较慢的两条线程其中的一条执行wait之后的代码,如此循环,保证一个时间点只有一条线程进行卖票,从而保证线程安全。
剩余票数 = 9, 线程 = {number = 6, name = (null)}
剩余票数 = 8, 线程 = {number = 4, name = (null)}
剩余票数 = 7, 线程 = {number = 5, name = (null)}
剩余票数 = 6, 线程 = {number = 6, name = (null)}
剩余票数 = 5, 线程 = {number = 4, name = (null)}
剩余票数 = 4, 线程 = {number = 5, name = (null)}
剩余票数 = 3, 线程 = {number = 6, name = (null)}
剩余票数 = 2, 线程 = {number = 4, name = (null)}
剩余票数 = 1, 线程 = {number = 5, name = (null)}
剩余票数 = 0, 线程 = {number = 6, name = (null)}
火车票售完
火车票售完
火车票售完
如果把信号量的代码注释,运行结果:剩余票数顺序明显不对,数据错乱。
剩余票数 = 8, 线程 = {number = 6, name = (null)}
剩余票数 = 7, 线程 = {number = 4, name = (null)}
剩余票数 = 9, 线程 = {number = 7, name = (null)}
剩余票数 = 6, 线程 = {number = 4, name = (null)}
剩余票数 = 6, 线程 = {number = 6, name = (null)}
剩余票数 = 5, 线程 = {number = 7, name = (null)}
剩余票数 = 4, 线程 = {number = 7, name = (null)}
剩余票数 = 4, 线程 = {number = 4, name = (null)}
剩余票数 = 4, 线程 = {number = 6, name = (null)}
剩余票数 = 3, 线程 = {number = 7, name = (null)}
剩余票数 = 3, 线程 = {number = 4, name = (null)}
剩余票数 = 3, 线程 = {number = 6, name = (null)}
剩余票数 = 2, 线程 = {number = 4, name = (null)}
剩余票数 = 1, 线程 = {number = 7, name = (null)}
剩余票数 = 0, 线程 = {number = 6, name = (null)}
火车票售完
火车票售完
火车票售完
六、线程安全
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。线程安全问题:一般使用线程同步技术,同步技术有加锁、串行队列、信号量等。
串行队列,上文已经有介绍,比如项目中FMDBQueue,就是在串行队列中同步操作数据库,从而保证线程安全。
信号量,上文已经有介绍,其实类似于互斥锁。
下面主要讲iOS中常见的锁:
从大的方向讲有两种锁:自旋锁、互斥锁。
自旋锁:线程反复检查锁变量是否可用,是一种忙等状态,自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。自旋锁的性能高于互斥锁,因为响应速度快,但是可能发生优先级反转问题(如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁)。常见的自旋锁包括:OSSpinLock、atomic等。
互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁,因此适用于线程阻塞时间较长的场合。常见的互斥锁包括:os_unfair_lock、pthread_mutex、dispatch_semaphore、@synchronized,其中pthread_mutex又衍生出NSLock、NSCondition、NSConditionLock、NSRecursiveLock,更加面向对象的锁。
至于锁的性能参考这个博客传送门,我只把测试结果放出来。
1、OSSpinLock
自旋锁OSSpinLock,已经在iOS10被苹果弃用,因为它存在优先级反转的问题。
#import
OSSpinLock lock = OS_SPINLOCK_INIT; //创建锁
OSSpinLockTry(&lock); //尝试加锁,加锁失败返回NO,成功返回YES
OSSpinLockLock(&lock); //加锁
// [self sellTickets];
OSSpinLockUnlock(&lock); //解锁
2、os_unfair_lock
这是苹果iOS10之后推出的新的取代OSSpinLock的锁。虽然是替代OSSpinLock的,但os_unfair_lock并不是自旋锁,而是互斥锁。
#import
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; //创建锁
os_unfair_lock_trylock(&lock); //尝试加锁,加锁失败返回NO,成功返回YES
os_unfair_lock_lock(&lock); //加锁
// [self sellTickets];
os_unfair_lock_unlock(&lock); //解锁
3、dispatch_semaphore
虽然上面有示例代码,但是这里也还是把思路写出来进行对比学习。
dispatch_semaphore_t lock = dispatch_semaphore_create(1); //创建锁
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); //加锁
// [self sellTickets];
dispatch_semaphore_signal(lock); //解锁
4、pthread_mutex
pthread_mutex是c底层的线程锁,关于pthread的各种同步机制,感兴趣的可以看看这篇文章pthread的各种同步机制,可谓讲的相当全面了。
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //创建锁
pthread_mutex_trylock(&lock); //尝试加锁,加锁失败返回NO,成功返回YES
pthread_mutex_lock(&lock); //加锁
// [self sellTickets];
pthread_mutex_unlock(&lock); //解锁
在YYKit的YYMemoryCache中,截取部分相关代码供大家参考。
pthread_mutex_t _lock; //声明
pthread_mutex_init(&_lock, NULL); //初始化
pthread_mutex_lock(&_lock); //加锁
NSUInteger count = _lru->_totalCount;
pthread_mutex_unlock(&_lock); //解锁
pthread_mutex_destroy(&_lock); //销毁锁
5、NSLock、NSCondition、NSConditionLock、NSRecursiveLock
以上四种锁全部是基于pthread_mutex封装的面向对象的锁。这几种锁都遵守NSLocking协议,此协议中提供了加锁 lock 和解锁 unlock 方法。
1)NSLock
比pthread_mutex更加面向对象,使用也很简单。
NSLock *lock = [NSLock new]; //创建锁
[lock tryLock]; //尝试加锁,加锁失败返回NO,成功返回YES
[lock lock]; //加锁
// [self sellTickets];
[lock unlock]; //解锁
在AFNetworking里的AFURLSessionManager.m文件里,截取一部分NSLock的用法供参考。
@property (readwrite, nonatomic, strong) NSLock *lock;
self.lock = [[NSLock alloc] init];
self.lock.name = AFURLSessionManagerLockName;
[self.lock lock];
delegate = self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)];
[self.lock unlock];
2)NSCondition
条件锁,它的使用和dispatch_semaphore有异曲同工之妙,API有wait和signal。
- (void)wait; // 线程等待
- (BOOL)waitUntilDate:(NSDate *)limit; // 设置线程等待时间,过了这个时间就会自动执行后面的代码
- (void)signal; // 唤醒一个设置为wait等待的线程
- (void)broadcast; // 唤醒所有设置为wait等待的线程
- (void)viewDidLoad {
[super viewDidLoad];
// 比如删除数组中的所有元素,等数组有元素时再去删除
NSCondition *lock = [[NSCondition alloc] init];
NSMutableArray *array = [[NSMutableArray alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{ //线程1
[lock lock];
while (!array.count) {
[lock wait]; //等待
}
[array removeAllObjects];
NSLog(@"array removeAllObjects");
[lock unlock];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{ //线程2
sleep(1); //以保证让线程2的代码后执行
[array addObject:@"Hello world!"];
NSLog(@"array addObject:Hello world!");
[lock signal]; //发送信号
});
}
3)NSConditionLock
条件锁,可以设置更多的条件。
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化NSConditionLock,并且condition设置为0
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
if([lock tryLockWhenCondition:0]){ //当condition为0时,尝试加锁
NSLog(@"Thread1 = %@", [NSThread currentThread]);
[lock unlockWithCondition:1]; //解锁,并且设置condition为1
}else{
NSLog(@"失败");
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lockWhenCondition:3];
NSLog(@"Thread2 = %@", [NSThread currentThread]);
[lock unlockWithCondition:2];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lockWhenCondition:1];
NSLog(@"Thread3 = %@", [NSThread currentThread]);
[lock unlockWithCondition:3];
});
}
运行结果:condition初始化是0,所以先加锁执行Thread1,然后解锁并且设置condition为1;接着当condition为1时,先加锁执行Thread3,然后解锁并且设置condition为3;最后类似的,执行Thread2。
根据NSConditionLock的这种特性,可以用来做多线程任务之间的依赖。
Thread1 = {number = 7, name = (null)}
Thread3 = {number = 4, name = (null)}
Thread2 = {number = 5, name = (null)}
4)NSRecursiveLock
递归锁,递归锁有一个特点,就是同一个线程可以加锁N次而不会引发死锁。
NSRecursiveLock *lock = [NSRecursiveLock new];
[lock tryLock];
[lock lock];
// [self sellTickets];
[lock unlock];
当然喽,pthread_mutex锁也支持递归,只需要设置PTHREAD_MUTEX_RECURSIVE即可。
pthread_mutex_t lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //绑定递归type
pthread_mutex_init(&lock, &attr); //初始化递归锁
pthread_mutexattr_destroy(&attr); //销毁attr
pthread_mutex_lock(&lock); //加锁
// [self sellTickets];
pthread_mutex_unlock(&lock); //解锁
pthread_mutex_destroy(&lock); //结束时销毁锁
6、pthread_rwlock
读写锁,经常用于文件等数据的读写操作,可以保证读写的正确性。如果需要实现“多读单写”功能,也可以用GCD的栅栏方法。
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; //创建读写锁
pthread_rwlock_rdlock(&rwlock); //读锁
pthread_rwlock_wrlock(&rwlock); //写锁
pthread_rwlock_unlock(&rwlock); //解锁
7、@synchronized
@synchronized,虽然在所有锁里面性能最差,但是代码最简单,所以项目实际使用很常见。
@synchronized (self) {
// [self sellTickets];
}
总结一下:
1)大部分锁都是先创建锁,然后加锁、解锁等流程;
2)如果对性能有要求,优先使用dispatch_semaphore或者pthread_mutex,如果对性能无要求,一般使用最简单的@synchronized即可。