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 的操作。


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) {
        // 进行处理
    }
}
@end

MyOperation *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"];
}
@end


MyOperation *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 小结_第1张图片

在线程之间共享的资源越多,使用的锁越多,程序被死锁的概率也越大。所以要尽量减少线程间资源共享,确保共享的资源尽量简单。


多线程注意事项

1、控制线程数量

使用并行队列,当任务过多且耗时较长时,队列会创建大量线程,而部分线程里面的耗时任务已经耗尽了 CPU 资源,所以其他的线程也只能等待 CPU 时间片,过多的线程也会让线程调度过于频繁。

GCD 中并行队列并不能限制线程数量,可以创建多个串行队列来模拟并行的效果。

2、减少队列切换

当线程数量超过 CPU 核心数量,CPU 核心通过线程调度切换用户态线程,意味着有上下文的转换(寄存器数据、栈等),过多的上下文切换会带来资源开销。虽然内核态线程的切换理论上不会是性能负担,开发中还是应该尽量减少线程的切换。

使用队列切换并不总是意味着线程的切换,代码层面可以减少队列切换来优化。

NSOperationQueue * queue=[[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        //...
    }];
}];


References

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

你可能感兴趣的:(NSOperation、NSOperationQueue 小结)