简介
iOS的多线程api一共有4套,从底层往上,分别是:
- p_thread :iOS基于unix操作系统,遵循POSIX标准,因此,最底层的多线程接口就是p_thread了。
- NSThread:基本上可以看成是p_thead的封装。
- GCD:可以理解为一种线程池化技术的实现。
- NSOperaion:GCD的封装,使用面向对象的方式管理任务和线程,提供了一些方便的api,代码可读性更高。
一:NSOperation
为什么要使用 NSOperation、NSOperationQueue?
- 可添加完成的代码块,在操作完成后执行。
- 添加操作之间的依赖关系,方便的控制执行顺序。
- 设定操作执行的优先级。
- 可以很方便的取消一个操作的执行。
- 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled
1:NSOpration
基本使用
NSOperation 表示任务,它本身是一个抽象类,如果要封装操作,需要使用它的两个子类,NSInvocationOperation 和 NSBlockOperation ,其中 NSInvocationOperation 使用SEL的方式执行任务,而NSBlockOperation则通过block执行任务。
- (void)useBlockOperation{
NSBlockOperation* op = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"Invocation operation finished %@",[NSThread currentThread]);
}];
[op start];
}
- (void)useInvocationOperation{
NSInvocationOperation* op = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(invocationRun)
object:nil];
[op start];
}
-(void)invocationRun{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"Invocation operation finished %@",[NSThread currentThread]);
}
operation 作为任务,默认并不会开辟更多的线程,因此在哪个线程调用start,就在哪个线程执行。
但是,如果给 NSBlockOperation添加多个任务,新添加的任务会开辟其他线程,也可以自定义并发的NSOperation,但个人不是太建议这么用,并发就应该交给 NSOperationQueue 。
- (void)useBlockOperation{
NSBlockOperation* op = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"Invocation operation finished %@",[NSThread currentThread]);
}];
//这个新添加的任务会开辟新的线程(这个估计是苹果为了方便做的api,后续讨论队列时,不包括这种情况)
[op addExecutionBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"Invocation operation finished %@",[NSThread currentThread]);
}];
[op start];
}
自定义NSOperation的子类
我们也可以自己定义NSOpetaion的子类,好处是在子类中,可以使用成员变量来保存函数执行的一些结果,在涉及某些递归函数或者函数调用比较复杂时,定义子类会比较好用。
使用方法比较简单,继承NSOperation后,重写main方法就好。
@interface TTOperation : NSOperation{
}
@end
@implementation TTOperation
-(void)main{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"tt operation finished %@",[NSThread currentThread]);
}
@end
2:NSOperatoinQueue
队列有两种:
主队列:所有操作都会放到主线程串行执行。
自定义队列:放到子队列执行,可以串行,也可以并行。
默认创建的队列是并行的,代码如下:
- (void)addOperationToQueue {
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 添加任务
// 添加 NSInvocationOperation
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationRun) object:nil];
// 添加 NSBlockOperation
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:0.5];
NSLog(@"block operation finished 3 %@",[NSThread currentThread]);
}
}];
// 使用快捷添加方式
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:0.5];
NSLog(@"block operation finished 3 %@",[NSThread currentThread]);
}];
// 使用 addOperation: 添加所有操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
}
3:基本操作
(1)设置最大并发数:
-1时为不限制,1为串行,大于1时为并发队列。这点用于控制网络请求的并发、任务负载均衡、挺有用的。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1; //1就是串行了
(2)操作依赖:
NSOperation* op1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"op1 finish");
}];
NSOperation* op2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:2.0];
NSLog(@"op1 finish");
}];
NSOperation* op3 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.5];
NSLog(@"op1 finish");
}];
NSOperation* op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"all finish");
}];
[op4 addDependency:op1];
[op4 addDependency:op2];
[op4 addDependency:op3];
NSOperationQueue* opq = [[NSOperationQueue alloc] init];
[opq addOperation:op1];
[opq addOperation:op2];
[opq addOperation:op3];
[opq addOperation:op4];
想象一个场景,同时下载多个任务,完成后通知用户下载完成,这个方法就很实用了。
(3)优先级
NSOperation* op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"log upload");
}];
[op setQueuePriority:NSOperationQueuePriorityLow];
如果任务1依赖于任务2,那么任务2完成之前,任务1处于阻塞状态,阻塞不属于优先级的考虑范围。但如果一个队列的并发数为5,同时有10个任务需要进行,那么优先级低的任务会排在后面。
这点在做日志上报之类功能的时候,尤其有用,毕竟不希望这类任务妨碍到用户正常使用,优先级一般是最低的。
(4)线程间通信
NSOperationQueue* opq = [[NSOperationQueue alloc] init];
[opq addOperationWithBlock:^{
[NSThread sleepForTimeInterval:1.0];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"回调主线程");
}];
}];
从这一点就可以看出来,NSOperation确实是对GCD的封装。
(5)线程同步和线程安全
NSOperation没有自己的锁api,所以使用任意一个锁的api都是可以的,比如 NSLock、@synchronized、NSRecursiveLock等等都可以。
4: 生命周期控制和自定义并发NSOperation
NSOperation有三种状态,isReady -> isExecuting -> isFinish
如果想执行完成后,再调用某个方法,比如刷新页面,可以添加如下方法。
[op setCompletionBlock:^{
NSLog(@"completion");
}];
NSOperation支持的KVO属性有:
- isCancelled
- isConcurrent (自定义必须重写)
- isExecuting (自定义必须重写)
- isFinished (自定义必须重写)
- isReady
- dependencies
- queuePriority
- completionBlock
如果要自定义并发NSOperation,要重写 start、main、isConcurrent方法,属性中需要包含 isExecuting 和 isFinish。
@interface MyOperation : NSOperation {
BOOL executing;
BOOL finished;
}
- (void)completeOperation;
@end
@implementation MyOperation
- (id)init {
self = [super init];
if (self) {
executing = NO;
finished = NO;
}
return self;
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
@end
- (void)start {
[self.lock lock];
// 在开始任务之前要测试一下是否取消
if ([self isCancelled])
{
// 如果是已经取消了,必需要把Finish设为YES
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
// 如果没有取消,就继续运行代码
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
[self.lock unlock];
}
- (void)main {
@try {
// 写你业务代码
[self completeOperation];
}
@catch(...) {
// Do not rethrow exceptions.
}
}
- (void)completeOperation {
[self.lock lock];
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
self.completionBlock();
[self.lock unlock];
}
二:GCD
1:基本操作
本质上,它就是一个iOS的线程池,全c实现,因此能够实现的功能比NSOperation要多,但是,对于复杂操作,没有NSOperation的实现那么清晰,容易理解。
GCD的性能比NSOperation好,优点如下:
- GCD 可用于多核的并行运算;
- GCD 会自动利用更多的 CPU 内核(比如双核、四核);
- GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
- 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
GCD的队列分为 串行队列 和 并发队列,串行队列中的任务一个个执行,并发的则是同步执行。
常用队列有两个,一个是主队列,这是一个串行队列,里面只包含一个主线程,一个是全局并发队列。
//并发
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
//串行
dispatch_queue_t serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
//全局
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
GCD的任务分为 同步任务 和 异步任务,同步任务会阻塞当前线程,异步则不会。
// 同步
dispatch_sync(queue, ^{
});
// 异步
dispatch_async(queue, ^{
});
这里有一个经典的死锁问题:阻塞当前线程的同时又想调用当前线程。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"我是死锁");
});
同步、异步,串行、并行 组合有许多种,没必要刻意去对比,记住基本含义就能推导出组合效果。比如串行队列加异步,串行队列中任务一个个执行,异步只是说,不阻塞当前线程(发起异步任务的线程)而已,所以最终的结果,就只有一个线程执行所有任务。
GCD线程通信和前面说的NSOperation差不多,毕竟NSOperation就是GCD的封装。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//执行一些操作...
dispatch_async(dispatch_get_main_queue(), ^{
//回调主线程
})
});
2:队列组
//执行任务:可以看见,队列组执行一个任务需要一个队列组、一个队列
dispatch_group_async_f(dispatch_group_t group,
dispatch_queue_t queue,
void *_Nullable context,
dispatch_function_t work);
//所有任务完成后,执行下面定义的block
dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
//队列组中的任务必须在指定时间内完成,时间一到,队列组中所有任务都将停止。
dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
这里面用的比较多的就是 dispatch_group_notify 了,比如使用多线程下载一个图片,全部下载完成后,就可以使用 dispatch_group_notify 通知当前线程完成图片拼接。但是,如果block中发起的是一个异步操作,比如如下代码,dispatch_group_notify无法保证所有操作已经完成。因为对于一个函数来说,其中发起的异步操作是由其他线程执行的,异步操作之后的代码不会等待异步操作的完成。
dispatch_queue_t global = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
dispatch_async(global, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"1");
});
});
dispatch_group_async(group, queue, ^{
dispatch_async(global, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"2");
});
});
dispatch_group_async(group, queue, ^{
dispatch_async(global, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"3");
});
});
dispatch_group_notify(group, queue, ^{
NSLog(@"finish");
});
上面的代码。finish会在 1、2、3之前打印。
这时候就需要使用 dispatch_group_enter 和 dispatch_group_leave
dispatch_queue_t global = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
dispatch_group_enter(group);
dispatch_async(global, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"1");
dispatch_group_leave(group);
});
});
dispatch_group_async(group, queue, ^{
dispatch_group_enter(group);
dispatch_async(global, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"2");
dispatch_group_leave(group);
});
});
dispatch_group_async(group, queue, ^{
dispatch_group_enter(group);
dispatch_async(global, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"3");
dispatch_group_leave(group);
});
});
dispatch_group_notify(group, queue, ^{
NSLog(@"finish");
});
notify方法会在所有 dispatch_group_leave 完成之后执行。
4:栅栏、延时执行、一次性代码、快速迭代
栅栏 : dispatch_barrier_async
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread sleepForTimeInterval:1.0];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread sleepForTimeInterval:1.0];
});
dispatch_barrier_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"barrier method");
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread sleepForTimeInterval:1.0];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread sleepForTimeInterval:1.0];
});
栅栏后的方法会等栅栏前的方法执行完成后再执行。
延迟执行:dispatch_after
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"after---%@",[NSThread currentThread]);
});
只执行一次的代码:dispatch_once
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行 1 次的代码(这里面默认是线程安全的)
});
这个在单例中用的很多。
同一个任务加入队列中,执行6次:dispatch_apply
dispatch_apply(6, dispatch_get_global_queue(0, 0), ^(size_t i) {
NSLog(@"index=%zd---thread=%@",i,[NSThread currentThread]);
});
三:信号量
这个就不细说了,如果看过操作系统原理很容易理解。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10); //信号量创建
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //等待,p操作 -1
dispatch_semaphore_signal(semaphore); //v操作 +1
3:NSThread
和前面两种不同,从NSThread这一层开始,就需要手动管理生命周期了。
作为p_thread,NSThread相对而言,提供的功能会比较多,过几天再写了,主要写一下runloop
NSThread* thread = [[NSThread alloc] initWithTarget:self selector:@selector(runloopTest) object:nil];
[thread start];
NSRunLoop* runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
[runloop run];