NSOperation、NSOperationQueue 小结


本篇文章将会简单介绍 iOS 多线程相关的内容。对 NSOperation、NSOperationQueue 的使用进行介绍总结。还将会介绍线程锁相关的内容。

iOS 多线程

多线程在开发中被广泛使用,创建多个线程,每个线程上同时执行不同的任务,从而更快更好使用 CPU 来进行工作。iOS 中提供了多种创建线程的方法,方便开发者操作使用。

1、pthread

POSIX 线程,定义了创建和操纵线程的一套 C语言的 API,使用方法如下:

//#import <pthread.h>

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    pthread_t thread;
    pthread_create(&thread, NULL, calculate, NULL);
}

void *calculate() {
    NSLog(@"%@", [NSThread currentThread]);
    return NULL;
}
//<NSThread: 0x600002d28c40>{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];
// <NSThread: 0x600000e17a80>{number = 8, name = 子线程} -- abc


//自启动创建子线程的方法
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"abc"];
//[NSThread detachNewThreadWithBlock:^{ }]; //iOS 10
//<NSThread: 0x600000b21040>{number = 8, name = (null)} -- abc


//为了更加简化我们创建一个子线程的操作, NSObject对创建线程封装了一些方法
//内部会自动的创建一个子线程,并且把@selector中的方法交给子线程去做,返回值void
[self performSelectorInBackground:@selector(run:) withObject:@"abc"];
// <NSThread: 0x600001fd4c40>{number = 8, name = (null)} -- abc

//[self performSelector:@selector(run:) withObject:@"abc"];
// <NSThread: 0x60000174cf00>{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"];
}];

// <NSThread: 0x600000015240>{number = 8, name = (null)} -- yyy
// on thread
// thread end sleep
// <NSThread: 0x600000015240>{number = 8, name = (null)} -- xxx
// <NSThread: 0x600000058f00>{number = 1, name = main} -- abc
// <NSThread: 0x600003bdaf00>{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]; //在主线程上运行,相当于同步执行
// <NSThread: 0x600003e8cf00>{number = 1, name = main} -- 123
NSBlockOperation
NSBlockOperation * bop=[NSBlockOperation blockOperationWithBlock:^{ 
        [self run:@"blockOperationWithBlock"]; 
}];
[bop addExecutionBlock:^{ 
        [self run:@"addExecutionBlock"]; 
}]; 
bop.completionBlock=^{ 
        NSLog(@"所有任务都执行完成了");  
};
[bop start];
// <NSThread: 0x600003e8cf00>{number = 1, name = main} -- blockOperationWithBlock
// <NSThread: 0x600003edd640>{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() -- <NSThread: 0x6000009780c0>{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 - <NSThread: 0x600000f54d80>{number = 1, name = main}
//do - <NSThread: 0x600000f0df00>{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 -- //不阻塞主线程,这个先打印
// <NSThread: 0x600000ce8380>{number = 7, name = (null)} -- queue add block
// <NSThread: 0x600002851140>{number = 5, name = (null)} -- queue bop
// <NSThread: 0x60000287cf00>{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 --
// <NSThread: 0x600002de8b00>{number = 7, name = (null)} --1
// <NSThread: 0x600002dbc180>{number = 5, name = (null)} -- barrier
// <NSThread: 0x600002dbd280>{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 --
// <NSThread: 0x600003550980>{number = 4, name = (null)} -- do something
// <NSThread: 0x60000351cd80>{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 等方法,这里不再介绍,自行查阅资料。

避免死锁

互斥锁解决了内存读写安全的问题,但这也引入了其他问题,其中一个就是死锁。当多个线程在相互等待着对方的结束时,就会发生死锁,这时程序可能会被卡住。

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


多线程注意事项

1、控制线程数量

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

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

2、减少队列切换

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

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

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


References

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345