在学习iOS多线程应用之前,我们先来学习一下什么是线程?
以上线程调度说的是单核设备,多核设备可以通过并行来同时执行多个线程
在iOS中有四种多线程方案,对比如下
方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 一套通用的多线程API 适用于Unix\Linux\Windows等系统 跨平台\可移植 使用难度大 |
C | 开发者手动管理 | 几乎不用 |
NSThread | 底层是pthread 使用更加面向对象 使用方便,可以执行操作线程对象 |
OC | 开发者手动管理 | 偶尔使用 |
GCD | 替代NSThread 充分利用设备的多核 |
C | 自动管理 | 常用 |
NSOperation | 对GCD的封装 使用更加面向对象 增加了一些使用功能 |
OC | 自动管理 | 常用 |
pthread是基于c语言的一套多线程API,正是因为底层是C语言,所以pthread能够在不同的操作系统上使用,移植性很强。但是pthread使用起来特别麻烦,而且需要手动管理线程的声明周期,因此基本很少使用,此处也不做过多介绍。
NSThread是苹果官方提供的一套操作线程的API,它是面向对象的,并且是轻量级的,使用灵活。但是和pthread一样,NSThread也需要开发者手动管理线程的生命周期。因此也很少使用,但是NSThread提供了一些非常实用的方法
#pragma mark - 线程创建 //获取当前线程 +(NSThread *)currentThread; //创建线程后自动启动线程 + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument; //线程休眠,可设置休眠结束时间 + (void)sleepUntilDate:(NSDate *)date; //线程休眠多久 + (void)sleepForTimeInterval:(NSTimeInterval)ti; //取消线程 - (void)cancel; //启动线程 - (void)start; //退出线程 + (void)exit; // 获得主线程 + (NSThread *)mainThread; //初始化方法 - (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0); //是否正在执行 - (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0); //是否执行完成 - (BOOL)isFinished NS_AVAILABLE(10_5, 2_0); //是否取消线程 - (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0); - (void)cancel NS_AVAILABLE(10_5, 2_0); //线程启动 - (void)start NS_AVAILABLE(10_5, 2_0); #pragma mark - 线程通信 //与主线程通信 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array; - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait; // equivalent to the first method with kCFRunLoopCommonModes //与其他子线程通信 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0); - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0); // equivalent to the first method with kCFRunLoopCommonModes //隐式创建并启动线程 - (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg NS_AVAILABLE(10_5, 2_0); 复制代码
NSThread的用法也非常简单,这里不做介绍,有兴趣的同学可以根据系统提供的API去进行尝试。
NSThread在平常开发中也有使用,例如我们经常使用[NSThread currentThread]来获取当前线程,使用[NSThread mainThread]来获取主线程。线程保活也是基于NSThread和RunLoop来实现的。
GCD是苹果为解决多核设备并行运算而提出的解决方案,它会合理的利用CPU多核的特性。并且GCD能够自动管理线程的生命周期(比如创建线程、任务调度、销毁线程等等),我们只需要告诉GCD具体要执行的任务,不需要编写任何关于线程的代码。同时GCD结合block使用更加简洁,因此在多线程开发中,GCD是首选。
在学习GCD之前,首先来学习两个比较重要的概念:任务和队列
任务其实就是我们需要执行的操作,在GCD中,我们通常将需要执行的操作放在block中。执行任务有两种方式:同步和异步。
因此,同步和异步最大的区别就是:是否具有开辟新线程的能力。
在GCD中,队列主要分为两种:串行队列和并发队列
串行队列和并发队列任务的插入方式都遵循FIFO(先进先出)原则,也就是新的任务总会插入到队列的末尾,但是串行队列中先进入队列的任务会先执行,并且等到任务执行完之后才会执行后面的任务。而并发队列则会同时执行队列中的多个任务,并且任务之间不会相互等待,任务的执行顺序和执行过程也不可预测。
GCD的使用步骤其实很简单,主要分为两个步骤
GCD中的队列有两种,串行队列和并发队列,除此之外,GCD还提供了两种特殊的队列,一种是主队列(其实就是一个串行队列),一种是全局队列(并发队列)。
创建队列是通过dispatch_queue_create函数,它有两个参数:
创建队列的代码如下:
//创建串行队列 dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); //创建并发队列 dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); //获取全局并发队列(参数1:队列优先级 参数二:保留字段,一般传0) dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //获取主队列 dispatch_queue_t mainQueue = dispatch_get_main_queue(); 复制代码
这里需要注意的是:主队列其实就是一个普通的串行队列,任何添加到主队列的任务都会在主线程中执行
GCD中,添加任务的方式也有两种,使用dispatch_sync创建同步任务和使用dispatch_async创建异步任务。不管是创建同步任务还是异步任务,都需要指定队列dispatch_queue_t
//创建串行队列 dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); NSLog(@"任务1"); dispatch_async(serialQueue, ^{ sleep(3); NSLog(@"任务2--%@",[NSThread currentThread]); }); NSLog(@"任务3"); dispatch_sync(serialQueue, ^{ sleep(1); NSLog(@"任务4--%@",[NSThread currentThread]); }); NSLog(@"任务5"); 复制代码
最终输出的结果如下:
任务1和任务3先打印,之后才会打印任务2。执行完任务2之后,才会执行任务4,并且执行完任务4,最后才会执行任务5。由此就可以验证上文中的结论:
//创建并发队列 dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); NSLog(@"任务1"); dispatch_async(concurrentQueue, ^{ NSLog(@"开始任务2"); sleep(3); NSLog(@"任务2--%@",[NSThread currentThread]); }); NSLog(@"任务3"); dispatch_sync(concurrentQueue, ^{ NSLog(@"开始任务4"); sleep(3); NSLog(@"任务4--%@",[NSThread currentThread]); }); NSLog(@"任务5"); 复制代码
执行结果如下:
队列存在两种:串行队列和并发队列,加上系统提供的主队列总共三种队列(此处由于主队列中添加的任务都会在主线程中执行,因此将主队列单独作为一种特殊的队列)。
任务又分为两种:同步任务和异步任务,因此队列加任务共有6种组合,所产生的效果及对比如下:
串行队列(手动创建) | 主队列 | 并发队列 | |
---|---|---|---|
同步任务(sync) | ●不会开辟新线程 ●串行执行任务 |
产生死锁 | ●不会开辟新线程 ●串行执行任务 |
异步任务(async) | ●开辟新线程 ●串行执行任务 |
●不会开辟新线程 ●串行执行任务 |
●开辟新线程 ●并发执行任务 |
还要注意一点:当使用sync向主队列中添加同步任务时,会产生死锁。此处暂时不考虑任务嵌套。
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"任务1"); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"任务2"); }); NSLog(@"任务3"); } 复制代码
执行图如下
首先,在执行viewDidLoad方法时,其实是将viewDidLoad添加到主队列中,因为viewDidLoad现在是在队首,所以先执行viewDidLoad方法。
viewDidLoad中有3个任务,都是在主线程中执行,当执行完任务1后,通过dispatch_sync方法又向主队列中添加了任务2(其实是整个block,这里暂且称为任务2),但是由于同步任务的特性是必现执行完且返回才能执行后面的任务,因此必须要执行完任务2才能执行后面的任务3。
此时在主队列中存在两个任务,viewDidLoad和任务2,任务2想要执行,就必须等待viewDidLoad执行完,而viewDidLoad想要执行完,必须要执行完任务2以及任务3,但是任务3想要执行,就必须执行完任务2,因此任务2在等待viewDidLoad执行完,viewDidLoad又在等待任务2执行完,从而造成死锁。
//创建串行队列 dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); dispatch_async(serialQueue, ^{ //此处称为block1 NSLog(@"任务1"); dispatch_sync(serialQueue, ^{ //此处称为block2 NSLog(@"任务2"); }); NSLog(@"任务3"); }); 复制代码
执行图如下:
首先,通过dispatch_async添加异步任务时会开启新的线程,所以此时block1中的任务是在子线程中执行,同时因为是在串行队列中增加的异步任务,所以block1会被添加到串行队列中去,并且在队首。
在子线程中执行block1中的方法,先执行任务1,然后执行dispatch_sync方法,此时会向串行队列中增加同步任务block2,并且需要等到block2执行完成之后才会执行任务3。
此时在串行队列中存在两个任务,block1和block2,block2想要执行,就必须等待block1执行完,而block1想要执行完,必须要执行完block2以及任务3,但是任务3想要执行,又必须执行完block2,因此block1在等待block2执行完,block2又在等待block1执行完,从而造成死锁。
//创建串行队列 dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); dispatch_sync(queue, ^{ NSLog(@"任务1"); dispatch_sync(queue, ^{ NSLog(@"任务2"); }); NSLog(@"任务3"); }); 复制代码
其实这种死锁的方式和第一种类似,同步任务还是在主线程执行,只不过被添加到了自定义的串行队列中,因此造成死锁的原因和第一种基本相同,这里不做介绍。
栅栏方法主要是在多组操作之间增加栅栏,从而分割多组操作,使得各组操作之间顺序执行。例如:有两组操作,需要执行完第一组操作之后再执行第二组操作,此时就需要用到dispatch_barrier_async,代码如下:
//创建并发队列 dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); //任务组一 for (int i = 0; i < 5; i++) { dispatch_async(concurrentQueue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"执行组一任务%d",i); }); } //栅栏方法 dispatch_barrier_sync(concurrentQueue, ^{ NSLog(@"栅栏方法"); }); //任务组二 for (int i = 0; i < 5; i++) { dispatch_async(concurrentQueue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"执行组二任务%d",i); }); } 复制代码
前提是所有的任务都需要添加到同一个队列中
执行结果如下:
可以看出任务组一中的5个任务并发执行,执行完成之后会先执行栅栏函数,最后才会执行任务组二中的所有操作,具体如下图:
队列组是一个非常实用的功能,它可以在一组异步任务都执行完成之后,再执行下一步操作。例如:有多个接口,需要等到所有的接口返回结果之后再到主线程更新UI。
队列组有三种使用方法:
- (void)testGroup1{ dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, concurrentQueue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"网络任务1:%@", [NSThread currentThread]); }); dispatch_group_async(group, concurrentQueue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"网络任务2:%@", [NSThread currentThread]); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"主线程更新UI:%@", [NSThread currentThread]); }); } 复制代码
- (void)testGroup2{ dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); dispatch_group_enter(group); dispatch_async(concurrentQueue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"网络任务1:%@", [NSThread currentThread]); dispatch_group_leave(group); }); dispatch_group_enter(group); dispatch_async(concurrentQueue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"网络任务2:%@", [NSThread currentThread]); dispatch_group_leave(group); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"主线程更新UI:%@", [NSThread currentThread]); }); } 复制代码
- (void)testGroup3{ dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, concurrentQueue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"网络任务1:%@", [NSThread currentThread]); }); dispatch_group_async(group, concurrentQueue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"网络任务2:%@", [NSThread currentThread]); }); //等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程) dispatch_group_wait(group, DISPATCH_TIME_FOREVER); dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"主线程更新UI:%@", [NSThread currentThread]); }); } 复制代码
以上三种方式的执行结果相同,如下:
信号量就是一种用来控制访问资源的数量的标识,当我们设置了一个信号量,在线程访问之前加上信号量的处理,就可以告知系统按照我们设定的信号量数量来执行多个线程。信号量其实是用计数来实现的,如果信号量计数小于0,则会一直等待,阻塞线程。如果信号量计数为0或者大于0,则不等待且计数-1。
GCD提供了三个方法来帮助我们使用信号量
函数 | 作用 |
---|---|
dispatch_semaphore_create | 创建信号量,初始值可以为0 |
dispatch_semaphore_signal | 发送信号,信号量计数+1 |
dispatch_semaphore_wait | 如果信号量>0,则使信号量-1,执行后续操作 如果信号量<=0,则会阻塞当前线程,直到信号量>0 |
示例代码如下:
- (void)testSemaphore{ dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); //信号量初始为0 dispatch_semaphore_t seq = dispatch_semaphore_create(0); NSLog(@"任务1"); dispatch_async(concurrentQueue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"任务2"); //信号量+1 dispatch_semaphore_signal(seq); }); //此时信号量小于0,所以一直等待,当信号量>=0时执行后续代码 dispatch_semaphore_wait(seq, DISPATCH_TIME_FOREVER); dispatch_async(concurrentQueue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"任务3"); //信号量+1 dispatch_semaphore_signal(seq); }); //信号量-1 dispatch_semaphore_wait(seq, DISPATCH_TIME_FOREVER); NSLog(@"任务4"); } 复制代码
执行结果如下:
首先会执行任务1,然后往并发队列中添加异步任务,之后执行dispatch_semaphore_wait时,信号量-1,此时信号量小于0(初始为0),因此线程被阻塞,一直在此处等待。当任务2执行完成后,会调用dispatch_semaphore_signal,此时信号量+1,程序继续往下执行。
因此,信号量也可以用来实现多个异步任务顺序执行,以及多个异步任务全部执行结束之后统一执行某些操作的需求。
NSOperation其实是对GCD更高一层的封装,完全面向对象,使用起来比GCD更加简单易用,代码的可读性也更高。并且NSOperation也提供了一些GCD没有提供的更加实用的功能。比如:
NSOperation是一个抽象类,不能直接使用。想要使用他的功能,就要使用它的子类NSInvocationOperation和NSBlockOperation。也可以自定义NSOperation的子类。
NSBlockOperation是将任务存放到block中,在合适的时机进行调用。
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"任务1:%@", [NSThread currentThread]); }]; [operation1 start]; 复制代码
并且NSBlockOperation还可以通过addExecutionBlock:方法添加额外操作,并且通过addExecutionBlock:添加的任务和通过blockOperationWithBlock:添加的任务可以在不同的线程中并发执行。
- (void)testBlock{ NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"主任务:%@",[NSThread currentThread]); }]; [op addExecutionBlock:^{ NSLog(@"附加任务1:%@",[NSThread currentThread]); }]; [op addExecutionBlock:^{ NSLog(@"附加任务2:%@",[NSThread currentThread]); }]; [op start]; } 复制代码
执行结果如下:
通过blockOperationWithBlock:创建的任务默认会在当前线程中同步执行,但是当blockOperationWithBlock:和addExecutionBlock:同时使用,并且addExecutionBlock:添加的任务足够多时,blockOperationWithBlock:创建的任务也会在子线程中执行。
通过addExecutionBlock:添加任务一定会开辟新的线程,在新线程中执行附加任务。
NSInvocationOperation可以指定target和selector
- (void)testOp{ NSInvocationOperation *invocationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(opeartion) object:nil]; [invocationOp start]; } - (void)opeartion{ NSLog(@"任务%@", [NSThread currentThread]); } 复制代码
默认情况下,NSInvocationOperation在调用start方法的时候不会开启线程,会在当前线程同步执行,只有当operation被添加到NSOperationQueue中才会开启新线程异步执行操作。
NSOperation可以设置任务之间的依赖,使任务按照预定的依赖顺序执行
- (void)testOp{ NSOperationQueue *queue = [[NSOperationQueue alloc] init]; NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"任务一:%@",[NSThread currentThread]); }]; NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"任务二:%@",[NSThread currentThread]); }]; NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testOp) object:nil]; //任务二依赖任务一 [op2 addDependency:op1]; //任务三依赖任务二 [op3 addDependency:op2]; [queue addOperations:@[op1, op2, op3] waitUntilFinished:NO]; } - (void)methond3{ NSLog(@"任务三:%@",[NSThread currentThread]); } 复制代码
原本三个任务是并发执行,但是添加完依赖之后就变成了顺序执行,如下:
此时因为3个任务顺序执行,所以只需开辟一条线程即可。
NSOperation中也有队列的概念,就是NSOperationQueue,通常NSOperationQueue和NSOperation会结合使用,一旦NSOperation被添加到NSOperationQueue时,会自动开辟新的线程异步执行
- (void)testOperation{ NSOperationQueue *queue = [[NSOperationQueue alloc] init]; NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"任务1:%@", [NSThread currentThread]); }]; [operation1 start]; NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"任务2, %@", [NSThread currentThread]); }]; [queue addOperation:operation2]; } 复制代码
执行结果如下:
可以看到,任务1没有添加到NSOperationQueue中,在主线程中执行,任务2添加到NSOperationQueue中,在子线程中执行。
注意:NSOperation添加到NSOperationQueue后会自动执行start方法,无需手动调用。
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //设置最大并发数 queue.maxConcurrentOperationCount = 1; for (int i = 0; i < 5; i++) { NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ NSLog(@"任务%d,%@",i, [NSThread currentThread]); }]; [queue addOperation:op]; } 复制代码
代码中将最大并发数设置为1,任务就会顺序执行,结果如下:
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //挂起任务 queue.suspended = YES; //恢复任务 queue.suspended = NO; //取消队列中所有任务(已经开始的无法取消) [queue cancelAllOperations]; 复制代码
//获取当前队列 [NSOperationQueue currentQueue]; //获取主队列 [NSOperationQueue mainQueue]; 复制代码
//设置操作优先级 @property NSOperationQueuePriority queuePriority; 复制代码
//操作是否正在执行 @property (readonly, getter=isExecuting) BOOL executing; //操作是否完成 @property (readonly, getter=isFinished) BOOL finished; //操作是否是并发执行 @property (readonly, getter=isConcurrent) BOOL concurrent; //操作是否是异步执行 @property (readonly, getter=isAsynchronous) BOOL asynchronous; //操作是否准备就绪 @property (readonly, getter=isReady) BOOL ready; 复制代码
//操作是否被取消 @property (readonly, getter=isCancelled) BOOL cancelled; //取消操作 - (void)cancel; 复制代码
//添加任务依赖 - (void)addDependency:(NSOperation *)op; //移除任务依赖 - (void)removeDependency:(NSOperation *)op; //获取当前任务的所有依赖 @property (readonly, copy) NSArray<NSOperation *> *dependencies; //阻塞任务执行线程,直到该任务执行完成 - (void)waitUntilFinished; //在当前任务执行完成之后调用completionBlock @property (nullable, copy) void (^completionBlock)(void); 复制代码
//添加单挑任务 - (void)addOperation:(NSOperation *)op; //添加多个任务 - (void)addOperations:(NSArray<NSOperation *> *)ops; //直接向队列中添加一个NSBlockOperation类型的操作 - (void)addOperationWithBlock:(void (^)(void))block; //在队列中的所有任务都执行完成之后会执行barrier block,类似栅栏 - (void)addBarrierBlock:(void (^)(void))barrier; 复制代码
//设置最大并发数 @property NSInteger maxConcurrentOperationCount; 复制代码
//挂起\恢复队列操作 YES:挂起 NO:恢复 @property (getter=isSuspended) BOOL suspended; //取消队列中所有操作 - (void)cancelAllOperations; //阻塞当前线程,直到队列中的操作全部执行完 - (void)waitUntilAllOperationsAreFinished; 复制代码
//获取当前队列 @property (class, readonly, strong, nullable) NSOperationQueue *currentQueue; //获取主队列 @property (class, readonly, strong) NSOperationQueue *mainQueue; 复制代码
在单线程条件下,任务都是串行执行,所以不存在安全问题,多线程能够极大的提高程序运行效率,但是多线程也存在隐患。当多个线程访问同一块资源时,非常容易引发数据错乱和数据安全问题。例如:现在有两条线程同时访问和修改同一个变量,如下:
线程A和线程B同时读取Integer的值,都为17,然后又同时对Integer的值+1,之后在修改Integer的值时由于线程A和线程B并发执行,因此两个线程会同时将Integer的值改为18,从而导致数据错乱。解决办法就是使用线程同步技术,就是让线程按预定的先后顺序依次执行。常见的线程同步技术是:加锁。以修改Integer的值为例,使用线程同步技术后结果如下:
线程A在访问Integer前先进行加锁操作,此时线程B无法访问Integer,然后线程A读取Integer的值,改为18,然后进行解锁,此时线程B就能够访问Integer,先进行加锁,读取Integer值为18,然后修改为19,最后再解锁。因此,使用加锁技术,就能够解决多线程的安全问题。
iOS中常见的线程同步技术有以下几种,我们以一个简单的Demo来对比一下这几种线程同步技术。
示例:假设现在银行账户上有5000元,使用多线程,分多次在银行账户上存钱取钱,保证最后银行存款正确。
如果我们使用多线程但是不使用线程同步技术的话,代码如下:
- (void)moneyTest{ __block int totalMoney = 5000; dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i = 0; i < 5; i++) { [NSThread sleepForTimeInterval:2]; totalMoney+=100; NSLog(@"存100,账户余额:%d",totalMoney); } }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i = 0; i < 5; i++) { [NSThread sleepForTimeInterval:2]; totalMoney-=200; NSLog(@"取200,账户余额:%d",totalMoney); } }); } 复制代码
如果按正常的流程,经过5次存钱和5次取钱,账户余额应该最终变为4500元,但是最终执行的结果缺大不相同,如下:
整个过程中账户余额的计算都有问题,同时,经过10次存取之后,账户余额还剩4700元,这就是多线程使用带来的弊端。
现在,我们就用以下技术来解决存钱取钱的问题
以下各种锁的实现可以在GNUstep中找到相应实现,虽然GNUstep不是官方源码,但是也具有一定的参考价值。
OSSpinLock叫做“自旋锁”,顾名思义,线程在等待解锁的过程中会处于忙等状态,并且一直会占用CPU资源。
OSSpinLock现在已经不再安全,因为它会出现优先级反转的问题,即优先级低的线程首先获得锁,进行加锁操作,CPU会给它分配资源来执行后续任务,如果此时有高优先级的线程进入,那么CPU会优先给高优先级的线程分配资源,此时低优先级线程得不到资源无法释放锁,而高优先级线程由于在等待低优先级线程解锁,而且是处于忙等状态,一直占用着CPU资源。因此就导致优先级反转的问题。
OSSpinLock具体Api如下:
#import <libkern/OSAtomic.h> //初始化锁 OSSpinLock lock = OS_SPINLOCK_INIT; //尝试加锁(如果需要等待,就不加锁,直接返回false,如果不需要等待就加锁,并且返回true) bool result = OSSpinLockTry(&lock); //加锁 OSSpinLockLock(&lock); //解锁 OSSpinLockUnlock(&lock) 复制代码
回到上述Demo,对存钱取钱操作进行加锁,如下:
- (void)moneyTest{ //初始化锁 __block OSSpinLock lock = OS_SPINLOCK_INIT; __block int totalMoney = 5000; dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i = 0; i < 5; i++) { OSSpinLockLock(&lock); //加锁 [NSThread sleepForTimeInterval:.1]; totalMoney+=100; OSSpinLockUnlock(&lock);//解锁 NSLog(@"存100,账户余额:%d",totalMoney); } }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i = 0; i < 5; i++) { OSSpinLockLock(&lock); //加锁 [NSThread sleepForTimeInterval:.1]; totalMoney-=200; OSSpinLockUnlock(&lock);//解锁 NSLog(@"取200,账户余额:%d",totalMoney); } }); } 复制代码
运行结果如下:
可以看到,整个过程按照顺序依次执行,先进行存钱,后进行取钱,最终账户余额为4500元,解决了数据错乱的问题。
os_unfair_lock被用来取代OSSpinLock,并且从iOS 10开始支持os_unfair_lock。等待锁的线程会处于休眠状态(不同于OSSpinLock的忙等状态),不会占用CPU资源。因此,使用os_unfair_lock不会导致优先级反转的问题。
os_unfair_lockApi如下:
#import <os/lock.h> //初始化锁 os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; //尝试加锁 bool result = os_unfair_lock_trylock(&lock); //加锁 os_unfair_lock_lock(&lock); //解锁 os_unfair_lock_unlock(&lock); 复制代码
使用方式同OSSpinLock。
pthread_mutex称为互斥锁,即当一个线程获得某一共享资源的使用权之后,会将该资源进行加锁,如果此时有其它线程想要获取该资源的锁,那么它将会被阻塞进入睡眠状态,直到该资源被解锁后才会唤醒。如果有多个线程尝试获取该资源的锁,那么它们都会进入睡眠状态,一旦该资源被解锁,这些线程就都会被唤醒,但是真正能获得资源使用权的是第一个被唤醒的线程。
使用互斥锁的线程在等待锁的过程中会处于休眠状态,不会占用CPU资源
pthread_mutex的Api如下:
#import <pthread.h> /* * Mutex type attributes */ #define PTHREAD_MUTEX_NORMAL 0 //默认类型,普通锁 #define PTHREAD_MUTEX_ERRORCHECK 1 //检错锁 #define PTHREAD_MUTEX_RECURSIVE 2 //递归锁 #define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL //初始化锁属性 pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); //初始化锁,第二个参数可以传NULL,就是使用默认的属性 pthread_mutex_t mutex; pthread_mutex_init(&mutex, &attr); //尝试加锁 pthread_mutex_trylock(&mutex); //加锁 pthread_mutex_lock(&mutex); //解锁 pthread_mutex_unlock(&mutex); //销毁 pthread_mutexattr_destroy(&attr); pthread_mutex_destroy(&mutex); 复制代码
使用pthread_mutex对存钱取钱进行加锁,如下:
- (void)moneyTest{ //初始化锁 __block pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); __block int totalMoney = 5000; dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i = 0; i < 5; i++) { pthread_mutex_lock(&mutex); //加锁 [NSThread sleepForTimeInterval:.1]; totalMoney+=100; pthread_mutex_unlock(&mutex);//解锁 NSLog(@"存100,账户余额:%d",totalMoney); } }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i = 0; i < 5; i++) { pthread_mutex_lock(&mutex); //加锁 [NSThread sleepForTimeInterval:.1]; totalMoney-=200; pthread_mutex_unlock(&mutex);//解锁 NSLog(@"取200,账户余额:%d",totalMoney); } }); //在不使用锁时需要调用此方法对锁进行销毁 //pthread_mutexattr_destroy(&mutex); } 复制代码
在初始化锁时,我们可以指定锁的类型为PTHREAD_MUTEX_RECURSIVE,此时我们就创建了一个递归锁。递归锁是指同一个线程可以多次获得某一个共享资源的锁(多次进行加锁操作),别的线程想要获取该资源锁,就必须等待该线程释放所有次数的锁。下面我们就创建一个递归函数的Demo来了解一下递归锁的使用:
#import "XLMutexRecursiveTest.h" #import <pthread.h> @interface XLMutexRecursiveTest () @property(nonatomic, assign)pthread_mutex_t mutex; @end @implementation XLMutexRecursiveTest - (instancetype)init { self = [super init]; if (self) { //递归锁:允许同一个线程对同一把锁进行重复加锁 pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); //pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //初始化锁 pthread_mutex_init(&_mutex, &attr); //销毁属性 pthread_mutexattr_destroy(&attr); } return self; } - (void)recursiveTask{ pthread_mutex_lock(&_mutex); NSLog(@"recursiveTask"); static int count = 0; if (count < 5) { count++; [self recursiveTask]; } pthread_mutex_unlock(&_mutex); } - (void)dealloc{ pthread_mutex_destroy(&_mutex); } @end 复制代码
首先创建普通锁PTHREAD_MUTEX_NORMAL,然后实例化XLMutexRecursiveTest实例进行调用
XLMutexRecursiveTest *recursiveTest = [[XLMutexRecursiveTest alloc] init]; [recursiveTest recursiveTask]; 复制代码
执行之后发现程序会一直卡死在第一次打印NSLog的地方。这是因为当首次执行recursiveTask方法时会对_mutex进行加锁,然后执行NSLog,当count < 5时,会再次执行recursiveTask方法,此时会发现_mutex已经被加锁了,因此第二次执行的recursiveTask方法会一直在等待解锁,而第一次执行的recursiveTask方法想要解锁,就必须要等第二次的任务执行完成,因此就造成了死锁。
下面我们将锁改成递归锁,重新执行,会发现所有的任务都正常打印了,如下
注意:在不使用pthread_mutex时要调用pthread_mutexattr_destroy和pthread_mutex_destroy对锁及其属性进行销毁。
条件变量是在多线程中用来实现“等待->唤醒”逻辑的常用方式,类似于GCD中的信号量。条件变量是利用一个全局共享变量来进行线程同步。它主要分为三步:
并且为了防止资源竞争,通常将条件变量和互斥锁结合使用。因为条件变量通常是多个线程或者进程的共享变量,所以就极有可能产生资源竞争,所以在使用条件变量之前需要对其加上互斥锁。pthread_mutex关于条件变量使用的Api如下:
//初始化锁 pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); //初始化条件 pthread_cond_t condt; pthread_cond_init(&condt, NULL); //等待条件(此时会进入休眠状态,同时对mutex进行解锁,被再次唤醒后,会对mutex再次加锁) pthread_cond_wait(&condt, &mutex); //激活一个等待该条件的线程 pthread_cond_signal(&condt); //激活所有等待该条件的线程 pthread_cond_broadcast(&condt); //销毁 pthread_cond_destroy(&condt); pthread_mutex_destroy(&mutex); 复制代码
条件变量比较典型的应用便是生产者-消费者模式,下面就模拟生产者-消费者来创建一个简单的Demo了解一下条件变量加互斥锁的使用,代码如下:
#import "XLMutexConditionLockTest.h" #import <pthread.h> @interface XLMutexConditionLockTest () //杯子余量 @property(nonatomic, strong)NSMutableArray *cupsRemain; @property(nonatomic, assign)pthread_mutex_t mutex; @property(nonatomic, assign)pthread_cond_t condt; @end @implementation XLMutexConditionLockTest - (instancetype)init { self = [super init]; if (self) { //初始化锁 pthread_mutex_init(&_mutex, NULL); //初始化条件 pthread_cond_init(&_condt, NULL); } return self; } - (void)testSaleAndProduce{ dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self _saleCup]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self _produceCup]; }); } //出首杯子 - (void)_saleCup{ pthread_mutex_lock(&_mutex); if (self.cupsRemain.count == 0) { //如果杯子余量为0,则等待生产杯子 NSLog(@"当前无可用杯子库存"); pthread_cond_wait(&_condt, &_mutex); } //此时有可出售的杯子 [self.cupsRemain removeLastObject]; NSLog(@"售出一个杯子"); pthread_mutex_unlock(&_mutex); } //生产杯子 - (void)_produceCup{ pthread_mutex_lock(&_mutex); //睡眠两秒,模拟生产过程 sleep(2); [self.cupsRemain addObject:@"yellow cup"]; NSLog(@"生产了一个黄色杯子"); //通知条件变量成立 pthread_cond_signal(&_condt); pthread_mutex_unlock(&_mutex); } - (void)dealloc{ //销毁 pthread_cond_destroy(&_condt); pthread_mutex_destroy(&_mutex); } @end 复制代码
执行结果如下:
可以发现,虽然_produceCup方法睡眠2s执行,但是_saleCup方法还是等待_produceCup执行完成之后再执行。由此可以总结出整个条件变量的流程如下:
dispatch_semaphore叫做信号量,前面讲GCD的时候也介绍过。dispatch_semaphore是通过设置一个全局的信号量,来控制线程并发访问的最大数量。假设信号量初始值为1,那么代表同时只允许1条线程访问资源,以此来保证线程同步。使用方式如下:
- (void)testDispatch{ //设置信号初始值 int semaphoreValue = 1; //初始化信号量 dispatch_semaphore_t semaphore = dispatch_semaphore_create(semaphoreValue); __block int totalMoney = 5000; dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i = 0; i < 5; i++) { //如果此时信号量<=0,那么dispatch_semaphore_wait会让线程处于休眠等待状态,直到信号量>0 //如果信号量>0,则执行dispatch_semaphore_wait会使信号量-1 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); [NSThread sleepForTimeInterval:.1]; totalMoney+=100; //会对信号量进行+1操作 dispatch_semaphore_signal(semaphore); NSLog(@"存100,账户余额:%d",totalMoney); } }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i = 0; i < 5; i++) { dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//信号量-1 [NSThread sleepForTimeInterval:.1]; totalMoney-=200; dispatch_semaphore_signal(semaphore); NSLog(@"取200,账户余额:%d",totalMoney);//信号量+1 } }); } 复制代码
初始信号量的值为1,此时,调用dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)方法,会判断信号量是否>0,如果信号量>0则会执行后续的操作,并且将信号量的值-1。如果信号量<=0,那么此方法会使当前线程处于休眠等待状态,直到信号量的值>0。
调用dispatch_semaphore_signal(semaphore)会使信号量+1,两种方法搭配使用就能实现线程同步的效果。
dispatch_queue(DISPATCH_QUEUE_SERIAL)其实就是一个串行队列,上文也说过,不管往串行队列中添加同步任务还是异步任务,在执行时都是串行执行任务。使用方式如下
- (void)testDispatchQueue{ //创建串行队列 dispatch_queue_t queue = dispatch_queue_create("lock_queue", DISPATCH_QUEUE_SERIAL); __block int totalMoney = 5000; dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [NSThread sleepForTimeInterval:.2]; totalMoney+=100; NSLog(@"存100,账户余额:%d",totalMoney); } }); dispatch_async(queue, ^{ for (int i = 0; i < 5; i++) { [NSThread sleepForTimeInterval:.2]; totalMoney-=200; NSLog(@"取200,账户余额:%d",totalMoney);//信号量+1 } }); } 复制代码
NSLock、NSRecursiveLock、NSCondition和NSConditionLock其实是对pthread_mutex中普通锁、递归锁和条件变量的封装,使其面向对象,使用起来更加简单。使用方式其实和pthread_mutex差不多,这里不做单独介绍了,只列出常用Api
@protocol NSLocking //加锁 - (void)lock; //解锁 - (void)unlock; @end @interface NSLock : NSObject <NSLocking> //尝试加锁 - (BOOL)tryLock; //给锁设置到期时间 - (BOOL)lockBeforeDate:(NSDate *)limit; @end 复制代码
@interface NSRecursiveLock : NSObject <NSLocking> //尝试加锁 - (BOOL)tryLock; //给锁设置到期时间 - (BOOL)lockBeforeDate:(NSDate *)limit; @end 复制代码
NSCondition 其实是封装了一个互斥锁和条件变量, 它把前者的 lock 方法和后者的 wait/signal 统一在 NSCondition 对象中,暴露给使用者。它的加锁和解锁过程同NSLock一致
@interface NSCondition : NSObject <NSLocking> - (void)wait; - (BOOL)waitUntilDate:(NSDate *)limit; - (void)signal; - (void)broadcast; @end 复制代码
NSConditionLock是对NSCondition的再一次封装,与NSCondition不同的是NSConditionLock可以设置具体的条件值
@interface NSConditionLock : NSObject <NSLocking> //带条件加锁 - (void)lockWhenCondition:(NSInteger)condition; //尝试加锁 - (BOOL)tryLock; - (BOOL)tryLockWhenCondition:(NSInteger)condition; //带条件解锁 - (void)unlockWithCondition:(NSInteger)condition; //设置锁到期时间 - (BOOL)lockBeforeDate:(NSDate *)limit; - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit; @end 复制代码
@synchronized内部其实封装了一个mutex递归锁。传入一个obj参数,内部会自动生成obj对应的递归锁,并且存放在哈希表中。通过obj的内存地址到哈希表中能拿到obj对应的递归锁。想要了解@synchronized内部实现,可以下载objc源码,查看objc_sync.mm文件中的objc_sync_enter和objc_sync_exit函数。
@synchronized的使用很简单,如下:
@synchronized (obj) { //需要加锁的代码 } 复制代码
将@synchronized应用到存钱取钱的案例中,如下:
- (void)testSynchronized{ __block int totalMoney = 5000; NSObject *obj = [NSObject new]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ @synchronized (obj) { for (int i = 0; i < 5; i++) { [NSThread sleepForTimeInterval:.2]; totalMoney+=100; NSLog(@"存100,账户余额:%d",totalMoney); } } }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ @synchronized (obj) { for (int i = 0; i < 5; i++) { [NSThread sleepForTimeInterval:.2]; totalMoney-=200; NSLog(@"取200,账户余额:%d",totalMoney); } } }); } 复制代码
传入的obj必须有值,如果obj传nil,则@synchronized(nil)不起任何作用。同时要实现多线程同步的话,就必须传入相同的obj
借用大神ibireme的不再安全的 OSSpinLock一文中关于各种锁的性能对比图,如下:
分辨自旋锁和互斥锁的方式,可以根据等待锁的过程中,线程是休眠还是忙等状态来区分。如果线程是休眠状态。就是互斥锁,如果是忙等状态,就是自旋锁。在OC中可以跟踪汇编代码来判断一个锁是自旋锁还是互斥锁。以OSSpinLock和os_unfair_lock为例来进行汇编代码跟踪:
#import "XLLockTest.h" #import <libkern/OSAtomic.h> #import <os/lock.h> @interface XLLockTest () @property(nonatomic, assign)OSSpinLock lock; @end @implementation XLLockTest - (instancetype)init{ self = [super init]; if (self) { _lock = OS_SPINLOCK_INIT; } return self; } - (void)test{ dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self thread2]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self thread1]; }); } - (void)thread1{ OSSpinLockLock(&_lock); NSLog(@"thread1"); OSSpinLockUnlock(&_lock); } - (void)thread2{ OSSpinLockLock(&_lock); sleep(60); NSLog(@"thread2"); OSSpinLockUnlock(&_lock); } 复制代码
断点在thread1方法,调用test方法,使用LLDB指令si一步一步执行汇编代码。首先进入OSSpinLockLock方法
在OSSpinLockLock方法内部调用了_OSSpinLockLockSlow函数
进入_OSSpinLockLockSlow函数,执行si指令,会发现,程序一直在循环执行一段汇编指令,如下:
熟悉汇编的同学可以看出其实这一段汇编代码就是一个while循环,由此就可以看出OSSpinLock属于自旋锁。
将Demo中的锁换成os_unfair_lock,然后用相同的方式跟踪汇编代码。首先是进入os_unfair_lock_lock方法,方法内部会调用_os_unfair_lock_lock_slow函数
_os_unfair_lock_lock_slow函数内部会调用__ulock_wait函数
在__ulock_wait函数内部会调用syscall,syscall其实就是系统级别的函数,执行完syscall函数之后,当前线程就会进入休眠状态。
由此就可以看出os_unfair_lock属于互斥锁。
自旋锁其实就是指当一个线程获取到资源锁之后,其它线程在获取资源锁时,会一直处于忙等状态(busy-waiting)。处于忙等状态的线程会一直处于活跃状态,但是内部并没有执行任何有效的任务,只是一直在循环查看资源锁拥有者是否已经释放了锁。
以下情况下适合使用自旋锁
互斥锁则是指当一个线程获取到资源锁之后,其它线程在获取资源锁时会被阻塞,进入睡眠状态(sleep-waiting)。线程休眠之后不会占用CPU资源,直到资源锁被释放之后才会唤醒线程。
以下情况下适合使用互斥锁
在OC中可以使用atomic或者nonatomic来修饰属性,代表原子性和非原子性。其实通俗一点来说,使用atomic修饰的属性是线程安全的,而使用nonatomic修饰的属性不是线程安全的。为什么说atomic修饰的属性是线程安全的呢?查看objc源码中的objc-accessors.mm文件可以看到atomic的底层实现,通过阅读源码可以发现,atomic修饰属性其实就是给属性的setter和getter方法内部增加了自旋锁,源码如下:
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { ...... if (!atomic) return *slot; //从全局的哈希表中获取到自旋锁 spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); id value = objc_retain(*slot); slotlock.unlock(); return objc_autoreleaseReturnValue(value); } void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) { ...... if (!atomic) { oldValue = *slot; *slot = newValue; } else { //从全局的哈希表中获取到自旋锁 spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } ...... } 复制代码
但是atomic只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。假设我们使用atomic修饰NSArray类型的属性
@property(atomic, strong)NSArray *sourceArray; 复制代码
如果多个线程对sourceArray进行添加数据操作,肯定会产生内存问题,因为atomic只是针对sourceArray本身的getter和setter方法,如果使用[_sourceArray objectAtIndex:index]时,就不是线程安全的,因为它和sourceArray的setter和getter方法没有关系。想要保证[_sourceArray objectAtIndex:index]的线程安全,就需要对_sourceArray的使用进行加锁操作。
不再安全的 OSSpinLock
深入理解iOS开发中的锁
以上内容纯属个人理解,如果有什么不对的地方欢迎留言指正。
一起学习,一起进步~~~