NSOperation、NSOperationQueue|NSOperation、NSOperationQueue 小结

原文链接:http://www.yupeng.fun/2020/03/29/nsoperation/


本篇文章将会简单介绍 iOS 多线程相关的内容。对 NSOperation、NSOperationQueue 的使用进行介绍总结。还将会介绍线程锁相关的内容。
iOS 多线程
多线程在开发中被广泛使用,创建多个线程,每个线程上同时执行不同的任务,从而更快更好使用 CPU 来进行工作。iOS 中提供了多种创建线程的方法,方便开发者操作使用。
1、pthread POSIX 线程,定义了创建和操纵线程的一套 C语言的 API,使用方法如下:
//#import - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { pthread_t thread; pthread_create(&thread, NULL, calculate, NULL); }void *calculate() { NSLog(@"%@", [NSThread currentThread]); return NULL; } //{number = 7, name = (null)}

2、NSThread NSThread 是 OC 对 pthread 的一个封装。通过封装,可以更方便的操作线程。
NSThread * thread=[[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"abc"]; //NSThread *thread = [[NSThread alloc] initWithBlock:^{ }]; //iOS 10 thread.name=@"子线程"; [thread start]; // {number = 8, name = 子线程} -- abc//自启动创建子线程的方法 [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"abc"]; //[NSThread detachNewThreadWithBlock:^{ }]; //iOS 10 //{number = 8, name = (null)} -- abc//为了更加简化我们创建一个子线程的操作, NSObject对创建线程封装了一些方法 //内部会自动的创建一个子线程,并且把@selector中的方法交给子线程去做,返回值void [self performSelectorInBackground:@selector(run:) withObject:@"abc"]; // {number = 8, name = (null)} -- abc//[self performSelector:@selector(run:) withObject:@"abc"]; // {number = 1, name = main} -- asdf//线程间通信 [NSThread detachNewThreadWithBlock:^{ [self run:@"yyy"]; NSLog(@"on thread"); [NSThread sleepForTimeInterval:2]; NSLog(@"thread end sleep"); [self performSelector:@selector(run:) withObject:@"xxx"]; [self performSelectorOnMainThread:@selector(run:) withObject:@"abc" waitUntilDone:YES]; //从子线程转回主线程 [self performSelectorInBackground:@selector(run:) withObject:@"123"]; }]; // {number = 8, name = (null)} -- yyy // on thread // thread end sleep // {number = 8, name = (null)} -- xxx // {number = 1, name = main} -- abc // {number = 9, name = (null)} -- 123- (void)run:(id)obj { NSLog(@"%@ -- %@", [NSThread currentThread], obj); }

使用 pthread 或者 NSThread 是直接对线程操作,可能会引发的一个问题,如果你的代码和所基于的框架代码都创建自己的线程,那么活动的线程数量有可能以指数级增长,每个线程都会消耗内存和内核资源。这样管理多个线程比较困难,所以不推荐在多线程任务多的情况下使用。
苹果官方推荐使用 GCD、NSOperation 和 NSOperationQueue ,这样就不用直接跟线程打交道了,只需要向队列中添加代码块即可,GCD 在后端管理着一个线程池。GCD 不仅决定着你的代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理。这样可以将开发者从线程管理的工作中解放出来,通过集中的管理线程,来缓解大量线程被创建的问题。
有关 GCD 的介绍可查看之前的文章。
下面来介绍有关 NSOperation 和 NSOperationQueue 的操作。
【NSOperation、NSOperationQueue|NSOperation、NSOperationQueue 小结】

3、NSOperation、NSOperationQueue NSOperation、NSOperationQueue 是 iOS 中一种多线程实现方式,实际上是基于 GCD 更高一层的封装,NSOperation 和 NSOperationQueue 分别对应 GCD 的任务和队列。面向对象,比 GCD 更简单易用。
3.1、NSOperation NSOperation是一个和任务相关的抽象类,不具备封装操作的能力,必须使用其子类 NSBlockOperation、NSInvocationOperation 或者使用自定义的继承自 NSOperation 的子类。
NSInvocationOperation
NSInvocationOperation *iop = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run:) object:@"123"]; [iop start]; //在主线程上运行,相当于同步执行 // {number = 1, name = main} -- 123

NSBlockOperation
NSBlockOperation * bop=[NSBlockOperation blockOperationWithBlock:^{ [self run:@"blockOperationWithBlock"]; }]; [bop addExecutionBlock:^{ [self run:@"addExecutionBlock"]; }]; bop.completionBlock=^{ NSLog(@"所有任务都执行完成了"); }; [bop start]; // {number = 1, name = main} -- blockOperationWithBlock // {number = 5, name = (null)} -- addExecutionBlock // 所有任务都执行完成了

如果添加的操作多的话,blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock: 中的操作一定会在当前线程中执行。addExecutionBlock: 的在哪个线程执行也不一定。一般都是把操作加入队列,通过队列来控制执行方式,对于线程的操作不用我们来处理。正如前面提到的,我们不用直接跟线程打交道,只需添加任务即可。
自定义 NSOperation 我们可以通过重写 main 或者 start 方法 来定义自己的 operations 。
重写 main 这种方法简单,不需要管理一些状态属性(例如 isExecuting 和 isFinished),当 main 方法返回的时候,这个 operation 就结束了。这种方式使用起来非常简单,但是灵活性相对重写 start 来说要少一些。
@interface MyOperation : NSOperation @end@implementation MyOperation - (void)main { NSLog(@"my operation main() -- %@", [NSThread currentThread]); //为了能使用操作队列所提供的取消功能, //在长时间操作中时不时地检查 isCancelled 属性 while (notDone && !self.isCancelled) { // 进行处理 } } @endMyOperation *op = [[MyOperation alloc] init]; [op start]; //my operation main() -- {number = 1, name = main}

如果想拥有更多的控制权,以及在一个操作中可以执行异步任务,可以通过重写 start 方法实现:
@interface MyOperation () @property (readonly, getter=isExecuting) BOOL executing; @property (readonly, getter=isFinished) BOOL finished; @end@implementation MyOperation@synthesize executing = _executing; @synthesize finished = _finished; - (void)start { self.executing = YES; self.finished = NO; NSLog(@"start - %@", [NSThread currentThread]); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [NSThread sleepForTimeInterval:2]; NSLog(@"do - %@", [NSThread currentThread]); [self done]; }); }- (void)done { self.finished = YES; self.executing = NO; }- (void)setFinished:(BOOL)finished { [self willChangeValueForKey:@"isFinished"]; _finished = finished; [self didChangeValueForKey:@"isFinished"]; }- (void)setExecuting:(BOOL)executing { [self willChangeValueForKey:@"isExecuting"]; _executing = executing; [self didChangeValueForKey:@"isExecuting"]; } @endMyOperation *op = [[MyOperation alloc] init]; [op start]; //start - {number = 1, name = main} //do - {number = 6, name = (null)}

这种情况下,你必须手动管理操作的状态。 为了让操作队列能够捕获到操作的改变,需要将状态的属性以配合 KVO 的方式进行实现。如果你不使用它们默认的 setter 来进行设置的话,你就需要在合适的时候发送合适的 KVO 消息。
3.2、NSOperationQueue NSOperationQueue 有两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。在两种类型中,这些队列所处理的任务都使用 NSOperation 的子类来表述。
NSInvocationOperation * iop1=[[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run:) object:@"queue iop1"]; NSBlockOperation * bop=[NSBlockOperation blockOperationWithBlock:^{ NSLog(@"%@ -- queue bop",[NSThread currentThread]); }]; //添加依赖关系 //iop1 依赖于 bop 一定是在 bop 任务执行完成之后才会执行 iop1 中的任务。相当于间接的设定了任务的执行顺序。 //看下面的打印内容,添加依赖的两个任务,在同一个线程中执行,顺序执行。这个不确定,不用管线程 [iop1 addDependency:bop]; //创建一个队列, 把任务交给队列管理 NSOperationQueue * queue=[[NSOperationQueue alloc] init]; [queue addOperation:iop1]; [queue addOperation:bop]; [queue addOperationWithBlock:^{ NSLog(@"%@ -- queue add block",[NSThread currentThread]); }]; //waitUntilFinished 是否等待队列中的执行任务完成之后再去执行后面的逻辑代码 //[queue addOperations:@[iop1, bop] waitUntilFinished:YES]; /** 不能重复加入队列,不然崩溃 reason: operations are finished, executing, or already in a queue, and cannot be enqueued' */NSLog(@" end -- "); //任务加入队列,队列创建子线程并发执行,不需要调用 start 方法 // end -- //不阻塞主线程,这个先打印 // {number = 7, name = (null)} -- queue add block // {number = 5, name = (null)} -- queue bop // {number = 5, name = (null)} -- queue iop1

添加依赖关系 [iop1 addDependency:bop];
iop1 依赖于 bop 一定是在 bop 任务执行完成之后才会执行 iop1 中的任务。相当于间接的设定了任务的执行顺序。
根据上面打印内容,添加依赖的两个任务,在同一个线程中执行,顺序执行。这个不确定,不用管线程。
maxConcurrentOperationCount queue.maxConcurrentOperationCount = 2 ;
用来控制一个特定队列中可以有多少个操作参与并发执行。
若将其设置为 1 的话,你将得到一个串行队列,这在以隔离为目的的时候会很有用。
addBarrierBlock 类似于 GCD 中的 dispatch_barrier_async 栅栏。类似分界线,阻碍后面的任务执行,直到 barrier block 执行完毕。
NSOperationQueue * queue=[[NSOperationQueue alloc] init]; [queue addOperationWithBlock:^{ NSLog(@"%@ --1 ",[NSThread currentThread]); }]; [queue addBarrierBlock:^{ NSLog(@"%@ -- barrier ",[NSThread currentThread]); }]; [queue addOperationWithBlock:^{ NSLog(@"%@ --2 ",[NSThread currentThread]); }]; NSLog(@" end -- "); // end -- // {number = 7, name = (null)} --1 // {number = 5, name = (null)} -- barrier // {number = 4, name = (null)} --2

操作之间的通信
NSOperationQueue * queue=[[NSOperationQueue alloc] init]; [queue addOperationWithBlock:^{ NSLog(@"%@ -- do something ",[NSThread currentThread]); [NSThread sleepForTimeInterval:2]; //任务完成,回到主线程 [[NSOperationQueue mainQueue] addOperationWithBlock:^{ NSLog(@"%@ -- completed ",[NSThread currentThread]); }]; }]; NSLog(@" end -- "); // end -- // {number = 4, name = (null)} -- do something // {number = 1, name = main} -- completed

线程安全
在多线程中访问共享资源,可能会遇到一些问题。比如,线程 A 和 B 都从内存中读取出了计数器的值,线程 A 将计数器值加一,同时线程 B 也将计数器值加一,这时计数器被加了两次,因为同时操作,结果只加一,这样就导致了数据的混乱。
为了防止出现这样的问题,多线程需要一种互斥的机制来访问共享资源,保证线程安全。
互斥访问就是同一时刻,只允许一个线程访问某个特定资源。为了保证这一点,每个希望访问共享资源的线程,首先需要获得一个共享资源的互斥锁,一旦某个线程对资源完成了操作,就释放掉这个互斥锁,这样别的线程就有机会访问该共享资源了。
加锁方式,常见的有,@synchronized、NSLock、dispatch_semaphore。
@synchronized
//创建两个操作,去访问 self.count NSOperationQueue * queue1=[[NSOperationQueue alloc] init]; [queue1 addOperationWithBlock:^{ NSLog(@"1 -- %@",[NSThread currentThread]); [self addCount]; }]; NSOperationQueue * queue2=[[NSOperationQueue alloc] init]; [queue2 addOperationWithBlock:^{ NSLog(@"2 -- %@ ",[NSThread currentThread]); [self addCount]; }];

- (void)addCount { @synchronized (self) { self.count += 1; } }

@synchronized (self) 括号里的 self 为该锁的标识,只有当标识相同时,才满足互斥。
NSLock
//self.lock = [[NSLock alloc] init]; - (void)addCount { [self.lock lock]; self.count += 1; [self.lock unlock]; }

NSLock 也是我们经常所使用的锁,除 lock 和 unlock 方法外,还有方法:
tryLock :尝试加锁,如果锁不可用(已经被锁住),刚并不会阻塞线程,并返回NO。
lockBeforeDate: 会在所指定 Date 之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。
类似的锁还有,NSConditionLock、NSRecursiveLock、NSCondition
dispatch_semaphore GCD 中的 dispatch_semaphore 信号量,也可以用来加锁。
//@property (strong, nonatomic, nonnull) dispatch_semaphore_t someLock; //self.someLock = dispatch_semaphore_create(1); - (void)addCount { dispatch_semaphore_wait(self.someLock, DISPATCH_TIME_FOREVER); //加锁 self.count += 1; dispatch_semaphore_signal(self.someLock); //解锁 }

1、dispatch_semaphore_create 函数可以生成信号量,参数是信号量计数的初始值。
2、dispatch_semaphore_wait 函数,当信号量值为 0 时等待,等待直到超时,参数可设置超时时长。信号量值大于等于 1 时,不等待,同时将信号量值减 1。
3、dispatch_semaphore_signal 函数会让信号量值加 1,如果有通过dispatch_semaphore_wait 函数等待信号量值增加的线程,会由系统唤醒最先等待的线程执行。
除了以上这些方法之外,还有 pthread_mutex、OSSpinLock 等方法,这里不再介绍,自行查阅资料。
避免死锁 互斥锁解决了内存读写安全的问题,但这也引入了其他问题,其中一个就是死锁。当多个线程在相互等待着对方的结束时,就会发生死锁,这时程序可能会被卡住。
NSOperation、NSOperationQueue|NSOperation、NSOperationQueue 小结
文章图片
在线程之间共享的资源越多,使用的锁越多,程序被死锁的概率也越大。所以要尽量减少线程间资源共享,确保共享的资源尽量简单。


多线程注意事项
1、控制线程数量 使用并行队列,当任务过多且耗时较长时,队列会创建大量线程,而部分线程里面的耗时任务已经耗尽了 CPU 资源,所以其他的线程也只能等待 CPU 时间片,过多的线程也会让线程调度过于频繁。
GCD 中并行队列并不能限制线程数量,可以创建多个串行队列来模拟并行的效果。
2、减少队列切换 当线程数量超过 CPU 核心数量,CPU 核心通过线程调度切换用户态线程,意味着有上下文的转换(寄存器数据、栈等),过多的上下文切换会带来资源开销。虽然内核态线程的切换理论上不会是性能负担,开发中还是应该尽量减少线程的切换。
使用队列切换并不总是意味着线程的切换,代码层面可以减少队列切换来优化。
NSOperationQueue * queue=[[NSOperationQueue alloc] init]; [queue addOperationWithBlock:^{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ //... }]; }];



References
苹果官网:Operation Queues
并发编程:API 及挑战
iOS中保证线程安全的几种方式与性能对比
iOS 如何高效的使用多线程

    推荐阅读