iOS开发中一共有四种多线程方案:Pthreads、NSThread、GCD、NSOperation。
- Pthreads:是线程的 POSIX 标准。该标准定义了创建和操纵线程的一整套 API,是基于 C 语言的框架。
- NSThread:是苹果封装后的,完全面向对象的类库。但是线程的生命周期还需要我们手动管理,不常用。
- GCD:是苹果为多核并行运算提供的线程方案,它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),基于 C 语言,使用 block,使用方便、灵活。
- NSOperation:是苹果对 GCD 的封装,完全面向对象。 NSOperationQueue 可以设置并发队列的依赖关系、执行顺序、最大并发数,更加灵活,解决复杂的线程设计。
GCD和NSOperation对比:
1. GCD更接近底层,处理速度更快;NSOperation是GCD的高级抽象。
2. GCD仅支持FIFO队列,而NSOperationQueue中的队列可以设置优先级、依赖关系、最大并发数。
3. NSOperationQueue支持KVO,我们可以观察任务的执行状态。
NSOperation详解
为什么要使用 NSOperation、NSOperationQueue?
- 可添加完成的代码块,在操作完成后执行。
- 添加操作之间的依赖关系,方便的控制执行顺序。
- 设定操作执行的优先级。
- 可以很方便的取消一个操作的执行。
- 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。
任务和队列
和GCD一样,NSOperation 中也有类似的任务(操作) 和 队列(操作队列) 的概念。
- 操作(NSOperation):NSOperation 只是一个抽象类,所以要使用子类 NSInvocationOperation、NSBlockOperation 来封装操作
- 操作队列(NSOperationQueue):指用来存放操作的队列,可以设置操作的优先级、操作的依赖关系和最大并发数;NSOperationQueue 提供了两种队列:主队列和自定义队列,主队列运行在主线程之上,而自定义队列在后台执行。
用NSOperation 实现多线程步骤:
- 创建操作:先将需要执行的操作封装到一个 NSOperation 对象中
- 创建队列:创建 NSOperationQueue 对象
- 将操作加入到队列中:将 NSOperation 对象添加到 NSOperationQueue 对象中
/**
* 基础实现方式
*/
- (void)addOperationToQueue {
// 获取主队列(凡是添加到主队列的操作,都会放到主线程中执行)一般不用
//NSOperationQueue *queue = [NSOperationQueue mainQueue];
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 使用 NSInvocationOperation 创建操作1
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 使用 NSBlockOperation 创建操作2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
}];
// 为 NSBlockOperation 添加额外操作
[op2 addExecutionBlock:^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
}];
// 使用 addOperation 添加所有操作到队列中
[queue addOperation:op1]; // [op1 start]
[queue addOperation:op2]; // [op2 start]
}
/**
* 简单实现方式(无需创建操作,直接block中添加操作)
*/
- (void)addOperationWithBlockToQueue {
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 使用 addOperationWithBlock 添加操作到队列中
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
}];
// 使用 addOperationWithBlock 添加操作到队列中
[queue addOperationWithBlock:^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
}];
}
操作依赖
NSOperationQueue 能添加操作之间的依赖关系,使我们可以很方便的控制操作之间的执行先后顺序
/**
* 操作依赖
*/
- (void)addDependency {
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 创建操作1
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
}];
// 创建操作2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
}];
// 添加依赖关系
[op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
// 添加操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
}
最大并发操作数(maxConcurrentOperationCount)
maxConcurrentOperationCount 控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而一个操作也并非只能在一个线程中运行。
- 默认情况下为-1,表示不限制,可进行并发执行。
- 为1时,队列为串行队列,只能串行执行。
- 大于1时,队列为并发队列,操作并发执行。当然这个值不应超过系统限制,即使自己设置一个很大的值,也会是系统的默认最大值。
优先级(queuePriority)
queuePriority 属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal,但我们可以通过 setQueuePriority 方法来改变当前操作在同一队列中的执行优先级。
- queuePriority 属性设置了进入准备就绪状态下的操作(无依赖)之间的开始执行顺序。优先级不能取代依赖关系。
- 如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。
- 如果一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作的优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。
线程安全
线程安全:可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。
iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock 等各种方式。
下面介绍一下使用 NSLock 对象来解决线程同步问题。NSLock 对象可以通过进入锁时调用 lock 方法,解锁时调用 unlock 方法来保证线程安全。
self.ticketSurplusCount = 50;
self.lock = [[NSLock alloc] init]; // 初始化 NSLock 对象
// 售卖火车票(线程安全)
- (void)saleTicketSafe {
while (1) {
// 加锁
[self.lock lock];
if (self.ticketSurplusCount > 0) {
//如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
}
// 解锁
[self.lock unlock];
if (self.ticketSurplusCount <= 0) {
NSLog(@"所有火车票均已售完");
break;
}
}
}
NSOperation、NSOperationQueue常用属性和方法
NSOperation:
- BOOL executing; //判断任务是否正在执行
- BOOL finished; //判断任务是否完成
- void (^completionBlock)(void); //用来设置完成后需要执行的操作
- (void)cancel; //取消任务
- (void)waitUntilFinished; //阻塞当前线程直到此任务执行完毕
NSOperationQueue:
- NSUInteger operationCount; //获取队列的任务数
- (void)cancelAllOperations; //取消队列中所有的任务
- (void)waitUntilAllOperationsAreFinished; //阻塞当前线程直到此队列中的所有任务执行完毕
- [queue setSuspended:YES]; // 暂停queue
- [queue setSuspended:NO]; // 继续queue
- (id)currentQueue; 获取当前队列
使用KVO观察任务执行状态
NSOperation 有三种状态:isReady -> isExecution -> isFinish。
- isReady: 返回 YES 表示操作已经准备好被执行。
- isExecuting: 返回YES表示操作正在执行。
- isFinished : 返回YES表示操作执行成功或者被取消了。
如果在 Ready 的状态中对 NSOperation 进行取消,NSOperation 会立刻进入 Finish 状态。
但如果操作已经在Executing状态,就会一直运行到结束,我们调用cancle方法系统不会中止线程,这需要我们在任务过程中检测取消事件,并中止线程的执行,还要注意释放内存或资源。
- (IBAction)startOperation:(id)sender {
self.blockOperation = [NSBlockOperation blockOperationWithBlock:^{
if ([self.blockOperation isCancelled]) {
NSLog(@"取消了");
return;
}
// 如果检测还没取消
// 继续请求网络,获取数据..
if ([self.blockOperation isCancelled]) {
NSLog(@"取消了");
return;
}
// 如果检测还没取消
// 继续将获取到了数据刷新界面...
}];
}
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:self.blockOperation];
}