NSOperation及NSOperationQueue
iOS多线程有多种方式如下:
- pthread :这是最底层的,基本不用。
- NSThread :就是我们说的线程,但是没有对线程的状态管理,线程依赖管理,同步与异步管理等。
- GCD:C系列的多线程管理,效率更高,代码更少,缺点是缺乏线程间的依赖管理和线程状态管理,适合我们只是简单的调用来做一些复杂的事情。
- NSOperation及NSOperationQueue:对GCD的封装,有对线程的状态管理,线程依赖管理,同步与异步管理。
NSOperation的优势:
- NSOperation是基于GCD之上的更高一层封装, 拥有更多的API(e.g. suspend, resume, cancel等等).
- 在NSOperationQueue中, 可以指定各个NSOperation之间的依赖关系.
- 用KVO可以方便的监测NSOperation的状态(isExecuted, isFinished, isCancelled).
- 更高的可定制能力, 你可以继承NSOperation实现可复用的逻辑模块.
并发编程的概念:
串行(Serial) VS. 并行(Concurrent)
串行和并行描述的是任务和任务之间的执行方式. 串行是任务A执行完了任务B才能执行, 它们俩只能顺序执行. 并行则是任务A和任务B可以同时执行.
同步(Synchronous) VS. 异步(Asynchronous)
同步和异步描述的其实就是函数什么时候返回. 比如用来下载图片的函数A: {download image}, 同步函数只有在image下载结束之后才返回, 下载的这段时间函数A只能搬个小板凳在那儿坐等... 而异步函数, 立即返回. 图片会去下载, 但函数A不会去等它完成. So, 异步函数不会堵塞当前线程去执行下一个函数!
并发(Concurrency) VS. 并行(Parallelism)
并发是程序的属性(property of the program), 而并行是计算机的属性(property of the machine),也就是说我们在编写程序时是让程序并发的进行,而计算机根据可用的核芯来计算是真正的多个核并行,还是一个核芯分时并行(假并行,由计算机调度).
NSOperation是一个抽象类,我们需要使用子类。系统默认有两个子类:
NSInvocationOperation和NSBlockOperation,两者区别从命名可以看出,一个是调用一个方法,一个是Block块,但是我们可以添加多个block,这些block是并发执行的,并且我们可以设置completionBlock,这就类似dispatch_group,直到多个block执行完才会执行completionBlock(后面我们都说并发,这是程序上的意义,但不一定并行,大多是一个线程分时并行)。
当然如果系统提供的不能满足需求,我们就要自己定义子类来实现相关功能。
当然他们共性是:我们使用start方法调用就会导致当前线程同步,就是说在哪个线程调用start方法,方法内的任务就会在这个线程执行,当然如果operation内又开启了线程做其他事,比如网络请求,start方法不会等待网络完成才返回。如果使用NSOperationQueue,无论什么队列都是异步的,因为它就是为异步设计的。
在我们自定义的NSOperation的子类内我们可以同时重写main方法和start方法,当我们直接调用start方法时调用顺序是默认先点用start方法,如果没有重写,才会去调用main方法,当然我们也可以直接调用main方法。如果我们把NSOperation子类添加到NSOperationQueue内,队列就会异步开启新的线程调用NSOperation子类start方法。如果加到主队列中也是异步主线程执行,不过会等待当前主线程空闲才会开始执行,我们可以使用这个特性,提前在一个方法书写最后需要执行的代码,这会比黑魔法attribute((cleanup(...)))还要晚,因为cleanup是在编译器优化时决定的执行顺序,与我们在最后作用域结束之前调用该方法一样,而主队列是属于串行队列,需等待主队列之前的任务完成才会执行,与作用域无关。这里就涉及到网络请求线程生命周期的维持,不建议切换到主线程来维持,这会导致等主线程的所有事情都做完了才会处理网络数据,可以采用AFNetworking的工作方式,自己重新开启一个常驻线程来负责网络返回数据的处理。
说到这里我们就需要简单介绍NSRunLoop:
应用启动后就会有一个与主线程对应的NSRunLoop来管理主线程的生命周期,这里还包括内存回收等。
我们平常开启的其它线程是没有NSRunLoop,故当我们使用一般的子线程去请求网络时,我们无法得到返回数据,因为子线程在完成网络的发起后就销毁了。以后在仔细说说NSRunLoop。
自定义 NSOperation 子类
说到自定义子类,作者也是试了千百遍,最重要的就是两个方法:willChangeValueForKey
和didChangeValueForKey
,在我们修改NSOperation的状态时我们会调用这两个方法,运用手动KVO通知(下面有介绍),这在建立NSOperation之间的依赖非常重要,否则依赖的下一方NSOperation永远无法知道上一方NSOperation已经结束。
其他的:isConcurrent或者isAsynchronous
,isFinished
,isExecuting
,isReady
,或者其他自定义状态都不是必须的,只不过给外界非NSOperation使用者知道状态变化,所用变化代码所带来的变化都需要我们自己编辑,我们可以参考AFNetworking,也是回到主线程发送相关通知来告诉需要知道状态变化的使用者,这里说到,之前一直以为我们在网络请求回来后无论失败或成功,任务是在辅助线程执行的,今天仔细查看源码才发现时在主线程执行的,其实在发起网络请求之前的处理都是在主线程,只是网络的建立和数据处理在常驻线程处理的。发现我们还可以自定义completionGroup
和completionQueue
,这样我们可以在所有成功或者失败处理完后我们可以通过自定义completionGroup
来通知外面执行completionBlock之后的任务,无论成功或者失败。
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
#pragma clang diagnostic ignored "-Wgnu"
self.completionBlock = ^{
if (self.completionGroup) {
dispatch_group_enter(self.completionGroup);
}
dispatch_async(http_request_operation_processing_queue(), ^{
if (self.error) {
if (failure) {
dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(self, self.error);
});
}
} else {
id responseObject = self.responseObject;
if (self.error) {
if (failure) {
dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(self, self.error);
});
}
} else {
if (success) {
dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
success(self, responseObject);
});
}
}
}
if (self.completionGroup) {
dispatch_group_leave(self.completionGroup);
}
});
};
#pragma clang diagnostic pop
使用KVO
Key-Value Observing机制
知识点介绍
Key-Value Observing (简写为KVO):当指定的对象的属性被修改了,允许对象接受到通知的机制。每次指定的被观察对象的属性被修改的时候,KVO都会自动的去通知相应的观察者。
KVO的优点:
当有属性改变,KVO会提供自动的消息通知。这样的架构有很多好处。首先,开发人员不需要自己去实现这样的方案:每次属性改变了就发送消息通知。这是KVO 机制提供的最大的优点。因为这个方案已经被明确定义,获得框架级支持,可以方便地采用。开发人员不需要添加任何代码,不需要设计自己的观察者模型,直接可 以在工程里使用。其次,KVO的架构非常的强大,可以很容易的支持多个观察者观察同一个属性,以及相关的值。
注意点:
cocoa的KVO模型中,有两种通知观察者的方式,自动通知和手动通知。
自动通知由cocoa在属性值变化时自动通知观察者,而手动通知需要在值变化时调用 willChangeValueForKey:和didChangeValueForKey: 方法通知调用者,且一个方法会触发一次通知,如果这样就会触发两次,所以在AFNetworking源码中,如果使用KVO通知外界使用者就会触发两次,这并不是我们希望的,故直接使用NSNotificationCenter通知。
要使用手动通知,需要在被观察者中重写 automaticallyNotifiesObserversForKey方法中明确告诉cocoa,哪些键值要使用自动通知,哪些不需要自动通知,自动通知只会通知一次,默认只要是可写的属性都是自动通知,只读的属性需要我们重写setter方法来手动通知。
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingOperationDidStartNotification object:self];
});
说到通知(这里包括KVO通知和NSNotificationCenter通知)与线程的关系,通知在哪个线程发出的,接受方(即注册该通知的观察者)执行的任务也会在相同的线程执行。
GCD
一般我们很少使用NSOperation,使用更方便更高效的GCD,关于GCD的介绍很多,这里简单介绍。值得注意的是GCD不是iOS专有的,它是在iOS 和 OSX 的核心 XNU 内核上实现的。
Dispatch Queue 分类
Serial Dispatch Queue
:串行的队列,每次只能执行一个任务,并且必须等待前一个执行任务完成,这也是最容易造成死锁的队列。
Concurrent Dispatch Queue
:一次可以并发执行多个任务,不必等待执行中的任务完成。
系统为我们提供了几种:
串行的队列:main queue。
并行的队列:四类(High,default,low,background),
当然我们可以自定义这两种队列。一般我么自定义的队列的优先级时默认的default优先级,我们可以使用dispatch_set_target_queue来调整优先级,这里就不细说。
看看下面两处代码的区别:
dispatch_async(serialQueue, ^{
NSLog(@"4");
dispatch_sync(serialQueue, ^{
NSLog(@"5");
});
NSLog(@"6");
});
dispatch_async(serialQueue, ^{
NSLog(@"4");
dispatch_async(serialQueue, ^{
NSLog(@"5");
});
for (int i = 0; i < 1000; i ++) {
NSLog(@"这里有很多任务%@",@(i));
}
NSLog(@"6");
});
很明显第一种就回造成死锁,第二种则会先打印4,1000个任务,6 ,最后再打印5,这就是串行队列,对于第一种情况,假设外面的任务为A,里面的任务为B,B任务要等待A任务执行完后才能执行,但是B任务是同步执行的,A需要等同步执行完才能执行后面,这就造成AB互相等待死锁;第二种情况就不一样,B任务是异步的,A任务不需要等待B返回就执行下面的任务,直到A执行结束,才轮到B执行。而所以我们在主线程上同步调用就会造成死锁。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"5");
});
延迟 dispatch_after
异步延迟调用,延迟时间并不是精确的时间,由于GCD是FIFO的,系统会根据当前的资源来开启相关的线程延迟执行任务。如果我们在主线程调用下面,由于是串行队列,需要等待主线程空闲时,才会开始延迟执行任务,我们可以放在全局队列中执行,这样就不用等待了。
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_global_queue(0, 0), ^{
NSLog(@"1");
});
单例dispatch_once
这个在单例初始化的时候是苹果官方推荐的方法。这个函数可以保证在应用程序中只执行指定的任务一次。即使在多线程的环境下执行,也可以保证百分之百的安全。
static id predicate_instance;
static dispatch_once_t predicate_once;
dispatch_once(&predicate, ^{
//your init
});
return instance;
}
dispatch_apply
这个就是对单线程或者串行队列下的循环的优化,一般我们如下写循环:
for (int i = 0; i < 1000; i ++) {
NSLog(@"这里有很多任务%@",@(i));
}
这样我们可以换成dispatch_apply,不过不是一条一条的执行,系统会帮我们建几个线程把这些任务分别调度到不同的线程的内执行:
dispatch_apply(1000, dispatch_get_global_queue(0, 0), ^(size_t i) {
NSLog(@"这里有很多任务%@",@(i));
});
使用时需要注意的是:
这个方法调用的时候会阻塞当前的线程,也就是同步执行的,如果我们希望把任务调度到主队列中按照顺序执行,你应该确保不是在主队列中调用,否则会造成死锁。我们可以如下由异步开启一个线程来调用:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_apply(1000, dispatch_get_main_queue(), ^(size_t i) {
NSLog(@"这里有很多任务%@",@(i));
});
});
你应该确保你要执行的各个任务是独立的,而且执行顺序也是无关紧要的,因为多个任务是同时进行的。
在你使用这个方法的时候,你还是要权衡下整体的性能的,如果你执行的任务时间比线程切换的时间还短。那就得不偿失了。
dispatch_suspend
&dispatch_resume
暂停队列,如果当前队列某个任务正在执行,则只能等待执行完才能暂停,要么在开始前就暂停,当然我们暂停后还需要恢复,否则相同队列中的其他任务不能执行,系统也会报错。
dispatch_group
在实际开发中,我们可能需要在一组操作全部完成后,才做其他操作。比如上传一组图片,或者下载多个文件。希望在全部完成时给用户一个提示。如果这些操作在串行化的队列中执行的话,那么你可以很明确的知道,当最后一个任务执行完成后,就全部完成了。这样的操作也并木有发挥多线程的优势。我们可以在并发的队列中进行这些操作,但是这个时候我们就不知道哪个是最后一个完成的了。这个时候我们可以借助dispatch_group:
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
//task1
NSLog(@"1");
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
//task2
NSLog(@"2");
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
//task3
NSLog(@"3");
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
//task4
NSLog(@"4");
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
//task5
NSLog(@"5");
});
dispatch_group_notify(group, queue, ^{
NSLog(@"finish");
})
// 阻塞当前线程,在指定的时间内等待group内的任务执行完才会执行后面的,根据result来判断是否执行完
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC);
long result = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
当然,如果我们并不需要全部并发,又部分任务时有先后顺序的,即是有依赖关系的,我们可以把任务调度到串行队列中:如下,由于GCD是FIFO的,先调度先执行,这样执行顺序task4 > task5 > task6,为了说明这点,在task5内添加了耗时任务,依然是这顺序,即串行队列先调度先执行,这样也弱弱模拟了依赖,当然依赖可以是多个线程任务之间的。
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t serialQueue = dispatch_queue_create("io.github.zhaojiewen", DISPATCH_QUEUE_SERIAL);
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
//task1
NSLog(@"1");
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
//task2
NSLog(@"2");
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
//task3
NSLog(@"3");
});
dispatch_group_async(group, serialQueue, ^{
//task4
NSLog(@"4");
});
dispatch_group_async(group, serialQueue, ^{
//task5
for (int i = 0; i < 10000; i ++) {
NSLog(@"这里有很多任务%@",@(i));
}
NSLog(@"5");
});
dispatch_group_async(group, serialQueue, ^{
//task6
NSLog(@"6");
});
对于添加到group的操作还有另外一个方法:
dispatch_group_enter(group);
dispatch_group_enter(group);
dispatch_async(defaultQueue, ^{
NSLog(@"1");
dispatch_group_leave(group);
});
dispatch_async(defaultQueue, ^{
NSLog(@"2");
dispatch_group_leave(group);
});
dispatch_group_notify(group, queue, ^{
NSLog(@"finish");
});
不过enter和leave必须成双成对出现。
线程同步dispatch_barrier_sync
或者dispatch_barrier_async
在多线程中一个比较重要的东西就是线程同步的问题。如果多个线程只是对某个资源只是读的过程,那么就不存在这个问题了。如果某个线程对这个资源需要进行写的操作,那这个时候就会出现同一个任务不同时刻数据不一致的问题了,不过队列必须同一个,不能使用系统的全局队列,那是不明确的,应该在统一的队列管理下。
一般的模式:
dispatch_queue_t queue = dispatch_queue_create("io.github.zhaojiewen", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_barrier_sync(queue, blk_for_writing);
dispatch_async(queue, blk3_for_reading);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);
Dispatch Semaphore
dispatch_semaphore_t
类似信号量,可以用来控制访问某一资源访问数量。
一般使用过程:
- 先创建一个Dispatch Semaphore对象,用整数值表示资源的可用数量
- 在每个任务中,调用dispatch_semaphore_wait来等待,如果信号量大于零则执行下面逻辑,并且信号量减1,否则一直阻塞,直到超时。
- 获得资源就可以进行操作
- 操作完后调用dispatch_semaphore_signal来释放资源,信号量加1,以便其他任务使用.
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"task1");
dispatch_semaphore_signal(semaphore);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"task2");
dispatch_semaphore_signal(semaphore);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"task3");
for (int i =0 ; i < 1000; i ++) {
NSLog(@"这里有多个任务%@",@(i));
}
dispatch_semaphore_signal(semaphore);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"task4");
dispatch_semaphore_signal(semaphore);
});
我们可以初始化信号量为零,当某个任务完成时信号量加一,然后其他任务才能开始执行,如下,耗时任务task3(比如网络请求)完成时,其他任务才开始。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"task1");
dispatch_semaphore_signal(semaphore);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"task2");
dispatch_semaphore_signal(semaphore);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"task3");
for (int i =0 ; i < 1000; i ++) {
NSLog(@"这里有多个任务%@",@(i));
}
dispatch_semaphore_signal(semaphore);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"task4");
dispatch_semaphore_signal(semaphore);
});
##线程间通信
最简单就是系统自带的几个方法:
`performSelector:`一系列
调研不同锁的效率
//
// hdf_TestLock.m
// RuntimeDemo
//
// Created by xuhaiqing on 16/1/15.
// Copyright © 2016年 huangyibiao. All rights reserved.
//
#import "hdf_TestLock.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <libkern/OSAtomic.h>
#import <pthread.h>
#define ITERATION (1024 * 1024 *32)
@implementation hdf_TestLock
+(void) start {
//初始化
double then , now;
unsigned int i ;
OSSpinLock osSpinLock = 0;
pthread_mutex_t pthread = PTHREAD_MUTEX_INITIALIZER;
NSLock *lock = [NSLock new];
NSRecursiveLock *recursiveLock = [NSRecursiveLock new];
then = CFAbsoluteTimeGetCurrent();
//NSRecursiveLock
for (i = 0; i < ITERATION ; i ++) {
@autoreleasepool {
[recursiveLock lock];
[recursiveLock unlock];
}
}
now = CFAbsoluteTimeGetCurrent();
NSLog(@"NSRecursiveLock : %f sec\n",now - then);
then = CFAbsoluteTimeGetCurrent();
then = CFAbsoluteTimeGetCurrent();
//NSLock
for (i = 0; i < ITERATION ; i ++) {
@autoreleasepool {
[lock lock];
[lock unlock];
}
}
now = CFAbsoluteTimeGetCurrent();
NSLog(@"NSLock : %f sec\n",now - then);
//直接运行时调用
//重置时间
then = CFAbsoluteTimeGetCurrent();
// IMP startlock = [NSLock instanceMethodForSelector:@selector(lock)];
//IMP endlock = [NSLock instanceMethodForSelector:@selector(unlock)];
for (i = 0; i < ITERATION; i ++) {
@autoreleasepool {
((void (*)(id,SEL))objc_msgSend)((id)lock,@selector(lock));
((void (*)(id,SEL))objc_msgSend)((id)lock,@selector(unlock));
}
}
now = CFAbsoluteTimeGetCurrent();
NSLog(@"NSLock Runtime : %f sec\n",now - then);
//pthread_mutex_unlock
//重置时间
then = CFAbsoluteTimeGetCurrent();
for (i = 0; i < ITERATION; i ++) {
@autoreleasepool {
pthread_mutex_lock(&pthread);
pthread_mutex_unlock(&pthread);
}
}
now = CFAbsoluteTimeGetCurrent();
NSLog(@"pthread_mutex_lock : %f sec\n",now - then);
//OSSpinLockLock
//重置时间
then = CFAbsoluteTimeGetCurrent();
for (i = 0; i < ITERATION; i ++) {
@autoreleasepool {
OSSpinLockLock(&osSpinLock);
OSSpinLockUnlock(&osSpinLock);
}
}
now = CFAbsoluteTimeGetCurrent();
NSLog(@"OSSpinLockLock : %f sec\n",now - then);
//重置时间
then = CFAbsoluteTimeGetCurrent();
//@synchronized
for (i = 0; i < ITERATION; i ++) {
@autoreleasepool {
@synchronized(self) {
}
}
}
now = CFAbsoluteTimeGetCurrent();
NSLog(@"synchronized : %f sec\n",now - then);
//重置时间
then = CFAbsoluteTimeGetCurrent();
//使用队列模拟锁
dispatch_queue_t queue = dispatch_queue_create("com.xuhaiqing.dreamwishing", DISPATCH_QUEUE_SERIAL);
for (i = 0; i < ITERATION; i ++) {
dispatch_sync(queue, ^{
});
}
now = CFAbsoluteTimeGetCurrent();
NSLog(@"DISPATCH_QUEUE_SERIAL : %f sec\n",now - then);
}
@end
调研日志:
NSRecursiveLock : 2.331977 sec
NSLock : 2.176556 sec
NSLock Runtime : 2.018537 sec
pthread_mutex_lock : 1.232123 sec
OSSpinLockLock : 0.878035 sec
synchronized : 3.668929 sec
DISPATCH_QUEUE_SERIAL : 1.382852 sec
从上面调研结果可以看出:
直接使用运行时的确提高了代码的运行效率,但是对开发者的要求提高了,一般在框架里使用。
自旋锁OSSpinLock效率 > 互斥锁pthread_mutex_lock > DISPATCH_QUEUE_SERIAL > NSLock Runtime > NSLock Runtime > NSLock > NSRecursiveLock > synchronized,其中DISPATCH_QUEUE_SERIAL是使用串行队列模拟的锁的功能。
- 互斥锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。
- 自旋锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。(这包括我们使用属性修饰符的atomic)