iOS多线程:『NSOperation、NSOperationQueue』详尽总结
iOS探索 多线程面试题分析
NSOperation、NSOperationQueue
是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue
是基于 GCD
更高一层的封装,完全面向对象。但是比 GCD
更简单易用、代码可读性也更高。
为什么要使用 NSOperation、NSOperationQueue?
KVO
观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled
。既然是基于 GCD
的更高一层的封装。那么,GCD
中的一些概念同样适用于 NSOperation、NSOperationQueue
。在 NSOperation、NSOperationQueue
中也有类似的任务(操作) 和 队列(操作队列) 的概念。
GCD
中是放在 block
中的。在 NSOperation
中,我们使用 NSOperation
子类 NSInvocationOperation、NSBlockOperation
,或者自定义子类来封装操作。GCD
中的调度队列 FIFO
(先进先出)的原则。NSOperationQueue
对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。(maxConcurrentOperationCount)
来控制并发、串行。NSOperationQueue
为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。NSOperation
需要配合 NSOperationQueue
来实现多线程。因为默认情况下,NSOperation
单独使用时系统同步执行操作,配合 NSOperationQueue
我们能更好的实现异步执行。
NSOperation
实现多线程的使用步骤分为三步:
NSOperation
对象中。NSOperationQueue
对象。NSOperation
对象添加到 NSOperationQueue
对象中。之后呢,系统就会自动将 NSOperationQueue
中的 NSOperation
取出来,在新线程中执行操作。
下面我们来学习下 NSOperation
和 NSOperationQueue
的基本使用。
NSOperation
是个抽象类,不能用来封装操作。我们只有使用它的子类来封装操作。我们有三种方式来封装操作。
NSInvocationOperation
NSBlockOperation
NSOperation
的子类,通过实现内部相应的方法来封装操作。在不使用 NSOperationQueue
,单独使用 NSOperation
的情况下系统同步执行操作,下面我们学习以下操作的三种创建方式。
//使用子类 NSInvocationOperation
- (void)useInvocationOperation {
NSLog(@"----%@", [NSThread currentThread]); // 打印当前线程
// 1.创建 NSInvocationOperation 对象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 2.调用 start 方法开始执行操作
[op start];
}
/**
* 任务1
*/
- (void)task1 {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"%d---%@", i, [NSThread currentThread]); // 打印当前线程
}
}
运行结果:
NSOperationQueue
、在主线程中单独使用使用子类 NSInvocationOperation
执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。如果在其他线程中执行操作,则打印结果为其他线程。
// 在其他线程使用子类 NSInvocationOperation
[NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];
运行结果如下:
NSInvocationOperation
,操作是在当前调用的其他线程执行的,并没有开启新线程。下边再来看看 NSBlockOperation
。
/**
* 使用子类 NSBlockOperation
*/
- (void)useBlockOperation {
NSLog(@"----%@", [NSThread currentThread]); // 打印当前线程
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"%d---%@", i, [NSThread currentThread]); // 打印当前线程
}
}];
// 2.调用 start 方法开始执行操作
[op start];
}
运行结果如下:
NSOperationQueue
、在主线程中单独使用 NSBlockOperation
执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。注意: 和上边 NSInvocationOperation
使用一样。因为代码是在主线程中调用的,所以打印结果为主线程。如果在其他线程中执行操作,则打印结果为其他线程
但是,NSBlockOperation
还提供了一个方法 addExecutionBlock:
,通过 addExecutionBlock:
就可以为 NSBlockOperation
添加额外的操作。这些操作(包括 blockOperationWithBlock
中的操作)可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完成执行时,才视为完成。
如果添加的操作多的话, blockOperationWithBlock:
中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock:
中的操作一定会在当前线程中执行。(可以使用 addExecutionBlock:
多添加几个操作试试):
/**
* 使用子类 NSBlockOperation
* 调用方法 AddExecutionBlock:
*/
- (void)useBlockOperationAddExecutionBlock {
// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 2.添加额外的操作
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"6---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"7---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"8---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.调用 start 方法开始执行操作
[op start];
}
运行结果如下:
NSBlockOperation
,并调用方法 AddExecutionBlock:
的情况下,blockOperationWithBlock:
方法中的操作 和 addExecutionBlock:
中的操作是在不同的线程中异步执行的。而且,这次执行结果中 blockOperationWithBlock:
方法中的操作也不是在当前线程(主线程)中执行的。从而印证了 blockOperationWithBlock:
中的操作也可能会在其他线程(非当前线程)中执行。一般情况下,如果一个 NSBlockOperation
对象封装了多个操作。NSBlockOperation
是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。
如果使用子类 NSInvocationOperation
、NSBlockOperation
不能满足日常需求,我们可以使用自定义继承自 NSOperation
的子类。可以通过重写 main
或者 start
方法 来定义自己的 NSOperation
对象。重写main
方法比较简单,我们不需要管理操作的状态属性 isExecuting
和 isFinished
。当 main
执行完返回的时候,这个操作就结束了
先定义一个继承自 NSOperation
的子类,重写main
方法。
// YSCOperation.h 文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KYOperation : NSOperation
@end
NS_ASSUME_NONNULL_END
//KYOperation.m文件
#import "KYOperation.h"
@implementation KYOperation
- (void)main {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
}
}
@end
然后使用的时候导入头文件KYOperation.h
。
/**
* 使用自定义继承自 NSOperation 的子类
*/
- (void)useCustomOperation {
// 1.创建 YSCOperation 对象
KYOperation *op = [[KYOperation alloc] init];
// 2.调用 start 方法开始执行操作
[op start];
}
运行结果如下:
NSOperationQueue
、在主线程单独使用自定义继承自 NSOperation
的子类的情况下,是在主线程执行操作,并没有开启新线程。另外我们再尝试一下start方法的重写:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KYOperation : NSOperation
@end
NS_ASSUME_NONNULL_END
#import "KYOperation.h"
@implementation KYOperation
- (void)start {
if (!self.isCancelled) {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
}
}
[super start];
}
@end
运行结果如下:
效果同上。
下边我们来讲讲 NSOperationQueue
的创建。
NSOperationQueue
一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。下边是主队列、自定义队列的基本创建方法和特点。
// 主队列获取方法
NSOperationQueue *queue = [NSOperationQueue mainQueue];
// 自定义队列创建方法
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
上边我们说到 NSOperation
需要配合 NSOperationQueue
来实现多线程
那么我们需要将创建好的操作加入到队列中去。总共有两种方法:
- (void)addOperation:(NSOperation *)op;
/**
* 使用 addOperation: 将操作加入到操作队列中
*/
- (void)addOperationToQueue {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作
// 使用 NSInvocationOperation 创建操作1
NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];
// 使用 NSInvocationOperation 创建操作2
NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];
// 使用 NSBlockOperation 创建操作3
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[op3 addExecutionBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.使用 addOperation: 添加所有操作到队列中
[queue addOperation:op1]; // [op1 start]
[queue addOperation:op2]; // [op2 start]
[queue addOperation:op3]; // [op3 start]
}
/**
* 任务1
*/
- (void)task1 {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"任务1:%d---%@", i, [NSThread currentThread]); // 打印当前线程
}
}
/**
* 任务2
*/
- (void)task2 {
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"任务2:%d---%@", i, [NSThread currentThread]); // 打印当前线程
}
}
运行结果如下:
NSOperation
子类创建操作,并使用 addOperation:
将操作加入到操作队列后能够开启新线程,进行并发执行。- (void)addOperationWithBlock:(void (^)(void))block;
block
中添加操作,直接将包含操作的 block
加入到队列中/**
* 使用 addOperationWithBlock: 将操作加入到操作队列中
*/
- (void)addOperationWithBlockToQueue {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.使用 addOperationWithBlock: 添加操作到队列中
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}
运行结果如下:
addOperationWithBlock:
将操作加入到操作队列后能够开启新线程,进行并发执行。之前我们说过,NSOperationQueue
创建的自定义队列同时具有串行、并发功能,上边我们演示了并发功能,那么他的串行功能是如何实现的?
这里有个关键属性 maxConcurrentOperationCount
,叫做最大并发操作数。用来控制一个特定队列中可以有多少个操作同时参与并发执行。
注意: 这里 maxConcurrentOperationCount
控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行
maxConcurrentOperationCount
maxConcurrentOperationCount
默认情况下为-1
,表示不进行限制,可进行并发执行maxConcurrentOperationCount
为1
时,队列为串行队列。只能串行执行maxConcurrentOperationCount
大于1
时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min
(min
为自己设定的值,系统设定的默认最大值)/**
* 设置 MaxConcurrentOperationCount(最大并发操作数)
*/
- (void)setMaxConcurrentOperationCount {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.设置最大并发操作数
queue.maxConcurrentOperationCount = 1; // 串行队列
// queue.maxConcurrentOperationCount = 2; // 并发队列
// queue.maxConcurrentOperationCount = 8; // 并发队列
// 3.添加操作
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}
最大并发操作数为1
的运行结果如下(一个一个执行):
最大并发操作数为2
的运行结果如下(两个两个执行):
1
时,操作是按顺序串行执行
的,并且一个操作完成之后,下一个操作才开始执行。当最大操作并发数为2
时,操作是并发执行
的,可以同时执行两个
操作。而开启线程数量是由系统决定的,不需要我们来管理。这样看来要比GCD简单很多
NSOperation、NSOperationQueue
最吸引人的地方是它能添加操作之间的依赖关系
。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。NSOperation
提供了3
个接口供我们管理和查看依赖。
- (void)addDependency:(NSOperation *)op;
添加依赖,使当前操作依赖于操作 op
的完成。- (void)removeDependency:(NSOperation *)op;
移除依赖,取消当前操作对操作 op
的依赖。@property (readonly, copy) NSArray *dependencies;
在当前操作开始执行之前完成执行的所有操作对象数组。当然,我们经常用到的还是添加依赖操作。现在考虑这样的需求,比如说有 A、B
两个操作,其中 A
执行完操作,B
才能执行操作。
如果使用依赖来处理的话,那么就需要让操作 B
依赖于操作 A
。具体代码如下:
/**
* 操作依赖
* 使用方法:addDependency:
*/
- (void)addDependency {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 2.创建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
// 3.添加依赖
[op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
// 4.添加操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
}
运行结果如下:
op1
先执行,op2
后执行NSOperation
提供了queuePriority
(优先级)属性,queuePriority
属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal
。但是我们可以通过setQueuePriority:
方法来改变当前操作在同一队列中的执行优先级。
// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
上边我们说过:对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)
那么,什么样的操作才是进入就绪状态的操作呢?
举个例子,现在有4
个优先级都是 NSOperationQueuePriorityNormal(默认级别)
的操作:op1,op2,op3,op4
。其中 op3
依赖于 op2
,op2
依赖于 op1
,即 op3 -> op2 -> op1
。现在将这4
个操作添加到队列中并发执行。
op1
和 op4
都没有需要依赖的操作,所以在 op1
,op4
执行之前,就是处于准备就绪状态的操作。op3
和 op2
都有依赖的操作(op3
依赖于 op2
,op2
依赖于 op1
),所以 op3
和 op2
都不是准备就绪状态下的操作。理解了进入就绪状态的操作,那么我们就理解了queuePriority
属性的作用对象。
queuePriority
属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。op1
和 op4
是不同优先级的操作,那么就会先执行优先级高的操作。在 iOS
开发过程中,我们一般在主线程里边进行 UI
刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
/**
* 线程间通信
*/
- (void)communication {
// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
// 2.添加操作
[queue addOperationWithBlock:^{
// 异步进行耗时操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
// 回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 进行一些 UI 刷新等操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}];
}
运行结果如下:
A
和 线程 B
一块配合,A
执行到一定程度时要依靠线程 B
的某个结果,于是停下来,示意 B
运行;B
依言执行,再将结果给 A
;A
再继续操作。举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)
下面,我们模拟火车票售卖的方式,实现 NSOperation
线程安全和解决线程同步问题。 场景:总共有50
张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止
先来看看不考虑线程安全的代码:
/**
* 非线程安全:不使用 NSLock
* 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
*/
- (void)initTicketStatusNotSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
self.ticketSurplusCount = 50;
// 1.创建 queue1,queue1 代表北京火车票售卖窗口
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;
// 2.创建 queue2,queue2 代表上海火车票售卖窗口
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;
// 3.创建卖票操作 op1
__weak typeof(self) weakSelf = self;
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicketNotSafe];
}];
// 4.创建卖票操作 op2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicketNotSafe];
}];
// 5.添加操作,开始卖票
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}
/**
* 售卖火车票(非线程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
if (self.ticketSurplusCount > 0) {
//如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"所有火车票均已售完");
break;
}
}
}
运行结果如下:
NSLock
情况下,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题。线程安全解决方案:可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。iOS
实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/ge
等等各种方式。这里我们使用 NSLock
对象来解决线程同步问题。NSLock
对象可以通过进入锁时调用 lock
方法,解锁时调用 unlock
方法来保证线程安全。
考虑线程安全的代码:
@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketSurplusCount;
@property (nonatomic, strong) NSLock *lock;
@end
/**
* 线程安全:使用 NSLock 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
self.ticketSurplusCount = 50;
self.lock = [[NSLock alloc] init]; // 初始化 NSLock 对象
// 1.创建 queue1,queue1 代表北京火车票售卖窗口
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;
// 2.创建 queue2,queue2 代表上海火车票售卖窗口
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;
// 3.创建卖票操作 op1
__weak typeof(self) weakSelf = self;
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicketSafe];
}];
// 4.创建卖票操作 op2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[weakSelf saleTicketSafe];
}];
// 5.添加操作,开始卖票
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}
/**
* 售卖火车票(线程安全)
*/
- (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;
}
}
}
运行结果如下:
NSLock
加锁、解锁机制的情况下,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题- (void)cancel;
可取消操作,实质是标记 isCancelled
状态- (BOOL)isFinished;
判断操作是否已经结束- (BOOL)isCancelled;
判断操作是否已经标记为取消- (BOOL)isExecuting;
判断操作是否正在在运行- (BOOL)isReady;
判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关- (void)waitUntilFinished;
阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步- (void)setCompletionBlock:(void (^)(void))block;
会在当前操作执行完毕时执行 completionBlock
- (void)addDependency:(NSOperation *)op;
添加依赖,使当前操作依赖于操作 op
的完成- (void)removeDependency:(NSOperation *)op;
移除依赖,取消当前操作对操作 op
的依赖@property (readonly, copy) NSArray *dependencies;
在当前操作开始执行之前完成执行的所有操作对象数组- (void)cancelAllOperations;
可以取消队列的所有操作- (BOOL)isSuspended;
判断队列是否处于暂停状态。 YES
为暂停状态,NO
为恢复状态- (void)setSuspended:(BOOL)b;
可设置操作的暂停和恢复,YES
代表暂停队列,NO
代表恢复队列- (void)waitUntilAllOperationsAreFinished;
阻塞当前线程,直到队列中的操作全部执行完毕- (void)addOperationWithBlock:(void (^)(void))block;
向队列中添加一个 NSBlockOperation
类型操作对象- (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait;
向队列中添加操作数组,wait
标志是否阻塞当前线程直到所有操作结束- (NSArray *)operations;
当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)- (NSUInteger)operationCount;
当前队列中的操作数+ (id)currentQueue;
获取当前队列,如果当前线程不是在 NSOperationQueue
上运行则返回 nil
+ (id)mainQueue;
获取主队列注意:
注意: 如果使用NSThread
的performSelector:withObject:afterDelay:
时需要添加到当前线程的runloop
中,因为在内部会创建一个NSTimer
GCD
和NSOperation
的关系如下:
GCD
是面向底层的C
语言的API
NSOperation
是用GCD
封装构建的,是GCD
的高级抽象GCD
和NSOperation
的对比如下:
GCD
执行效率更高,而且由于队列中执行的是由block
构成的任务,这是一个轻量级的数据结构——写起来更加方便GCD
只支持FIFO
的队列,而NSOpration
可以设置最大并发数、设置优先级、添加依赖关系等调整执行顺序NSOpration
甚至可以跨队列设置依赖关系,但是GCD
只能通过设置串行队列,或者在队列内添加barrier
任务才能控制执行顺序,较为复杂NSOperation
支持KVO
(面向对象)可以检测operation
是否正在执行、是否结束、是否取消UI
dispatch_get_main_queue()
回到主线程刷新UI
dispatch_group
统一调度刷新UI
dispatch_once
method-Swizzling
使用保证方法只交换一次dispatch_after
将任务延迟加入队列dispatch_semaphore_t
GCD
的最大并发数dispatch_source
定时器替代误差较大的NSTimer
AFNetworking、SDWebImage
等知名三方库中的NSOperation
使用内容学习自这篇博客:对iOS中自旋锁与优先级反转(Priority inversion)的理解
关于优先级反转,参考资料中《优先级反转那点事儿》讲的比较清晰。此处直接贴过来
A
在一个比较低的优先级上工作, 假设是10
吧。然后在时间点T1
的时候,线程A
锁定了一把互斥锁,并开始操作互斥数据。C
(比如优先级20
)在时间点T2
被唤醒,它也也需要操作互斥数据。当它加锁互斥锁时,因为互斥锁在T1
被线程A
锁掉了,所以线程C
放弃CPU
进入阻塞状态,而线程A
得以占据CPU
,继续执行。10
的A
线程看上去抢了优先级20
的C
线程的时间,但因为程序逻辑,C确实需要退出CPU
等完成互斥数据操作后,才能获得CPU
。B
在优先级15
上,在T3
时间点上醒了过来,因为他比当前执行的线程A
优先级高,所以它会立即抢占CPU
。而线程A
被迫进入READY
状态等待。T4
,线程B
放弃CPU
,这时优先级10
的线程A
是唯一READY
线程,它再次占据CPU
继续执行,最后在T5
解锁了互斥锁。T5
,线程A
解锁的瞬间,线程C
立即获取互斥锁,并在优先级20
上等待CPU
。因为它比线程A
的优先级高,系统立刻调度线程C
执行,而线程A
再次进入READY
状态。上面这个时序里,线程B
从T3
到T4
占据CPU
运行的行为,就是事实上的优先级反转。一个优先级15
的线程B
,通过压制优线级10
的线程A
,而事实上导致高优先级线程C
无法正确得到CPU
。这段时间是不可控的,因为线程B
可以长时间占据CPU
(即使轮转时间片到时,线程A
和B
都处于可执行态,但是因为B
的优先级高,它依然可以占据CPU
),其结果就是高优先级线程C
可能长时间无法得到 CPU
。
iOS
中的自旋锁OSSpinLock
现已被废弃,官方推荐使用os_unfair_lock
互斥锁。
另外GCD
中的信号量方法也由于不会记录持有它的线程信息,当发生优先级反转的时候,系统找不到优先级的线程,导致系统可能无法通过提高优先级解决优先级反转问题,故也不建议使用。