iOS开发多线程-NSOperation / GCD详解

2018-02-09 12:46:01来源:https://www.jianshu.com/p/9b23e985fb74作者:Mister志伟人点击

分享


iOS多线程开发必须知道的概念名词:
1. 进程
进程(process)就是一个正在执行的程序的实例。也就是说我们的每一个APP程序在执行时实例都是一个进程,也可以说在APP执行时,它只拥有唯一的一个进程。
每一个进程都是独立的,每一个进程均在专属的内存空间内,iOS中每一个App(一个进程)都有自己独特的内存和磁盘空间,别的App(进程)是不允许访问的(越狱除外)。iOS开发中应用程序之间互相调用包括调用发短信、打电话等进程相关操作的API都被封装到UIApplcation这个类中了。
每个进程至少拥有一个线程。
在UNIX和Linux系统中是有进程层次结构的,当进程创建了另一个进程后,父进程和子进程就以某种方式继续保持联系。子进程自身可以创建更多的进程,组成一个进程的层次结构。(进程只有一个父进程但是可以有0个,1个或多个子进程。)而Windows中没有进程层次的概念,所有进程地位相同。唯一类似进程层次的地方是在创建进程的时候父进程得到一个特别的令牌(称为句柄),该令牌可以用来控制子进程。但是父进程可以把这个令牌传送给其他进程,这样就不存在进程层次了。
进程有3种状态,分别是:
1)运行态(该时刻进程实际占用CPU)。
2)就绪态(可运行,但因为其他进程正在运行而暂时停止)。
3)阻塞态(除非某种外部事件发生,否则进程不能运行)。
2. 线程
进程用于把资源集中到一起,而线程是在CUP上被调度的实体。
线程拥有自己的寄存器,用来保存当前的工作变量,称作线程的上下文。还拥有一个自己的堆栈,用来记录执行历史。
线程之间共享同样的内存空间和全局变量,一个线程可以读、写或甚至清除另一个线程的堆栈。
如果多个线程都是CPU密集型(也称计算密集型)那并不能获得性能上的增强,如果是I/O密集型则能加快程序执行速度。
进程和线程的关系
线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,它们共享进程的地址空间。一个线程crash就等于整个进程crash。

进程的创建和操作开销很大,某种意义上讲线程就相当于轻量级的进程。多线程使用的其中一个理由就是因为线程比进程更轻量级,线程比进程更容易(即更快)的创建和撤销。在许多系统中,创建一个线程比创建一个进程要快10~100倍。





进程线程关系示意图.jpeg

多线程好处多的同时也易引发一些问题,“数据竞争”-多个线程操作同一资源时可能会导致数据的不一致;“死锁”-两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象;使用太多的线程会消耗大量内存,因为每个线程都有自己的寄存器。进程太多的时候也有消耗太大的问题。


3. 串行(serial) & 并发(concurrent)& 并行

串行是同步线程的实现方式,就是任务A执行结束才能开始执行B,单个线程只能执行一个任务。
并发并行其实是异步线程实现的两种形式。并行其实是真正的异步,多核CUP可以同时开启多条线程供多个任务同时执行,互不干扰。并发是伪异步,单个CUP一个时刻只能有一个线程执行,想执行多个任务就必须不断切换执行任务的线程。


4. 同步 & 异步

同步:多个任务情况下,一个任务A执行结束,才可以执行另一个任务B。只存在一个线程。
异步:多个任务情况下,一个任务A正在执行,同时可以执行另一个任务B。任务B不用等待任务A结束才执行。存在多条线程。


5. 调度队列(Dispatch Queue)

调度队列是执行处理的队列也是GCD的基本概念,它按照执行任务添加的顺序(即FIFO-先进先出顺序)执行处理。调度队列在执行处理时存在两种Dispatch Queue,一种是等待现在执行中处理的Serial Dispatch Queue,另一种是不等待现在执行中处理的Concurrent Dispatch Queue,稍后会对这两种队列详细的介绍。官方的说法是有三种队列,还要一种叫Main dispatch queueMain dispatch queue其实也可以归为Serial Dispatch Queue,不过由于它是主线程队列所以单拿了出来。


iOS多线程技术对比

pthread

pthread(POSIX thread)是一套通用的多线程API,适用于Unix、Linux、Windows等系统,跨平台、可移植的C语言框架,线程生命周期由开发者管理,使用难度大。GCD的底层实现库中也有用到Libc(pthreads)



NSThread

NSThread是这几种方法里面相对轻量级的,但需要管理线程的生命周期、同步、加锁问题,这会导致一定的性能开销,同时在多个线程开发时不便于开发维护。详细使用可参考这篇博客。



NSOperation

NSOperation是基于OC实现的,它以面向对象的方式封装了需要执行的操作,然后可以将这个操作放到一个NSOperationQueue中去异步执行。它是线程安全的,开发者不必关心线程管理、同步等问题。
NSOperation类是一个抽象类来封装一个任务相关的代码和数据,不能直接被使用。可以使用它的两个子类
NSInvocationOperationNSBlockoperation来执行实际的任务,当然你也可以自己封装一个子类来实现(只需要重载-(void)main这个方法,在这个方法里面添加需要执行的操作。)。
NSOperation可以取消添加的执行任务。一个NSOperation对象是一个单次对象(single-shot object)只能执行一次任务,不能再次执行它。

GCD

Grand Central Dispatch(GCD)是苹果开发的基于XNU内核级线程管理技术,优化对多核处理器的支持。
GCD是一个基于线程池的任务并行执行模式。其基本思想是将线程池的管理从开发人员手中转移出来,并更接近操作系统(更高效)。开发人员不用管理线程,只需要把任务添加到执行队列就可以。
GCD基于C语言实现,不过使用了Block,因而API非常简洁易用。
GCD需要开发者释放自己创建的队列Dispatch Queue,系统提供的标准队列是全局的所以不用释放。
NSOperation

NSOperation实现多线程主要步骤是:
1> 封装执行的操作到一个NSOperation对象中


2> 将封装的NSOperation对象添加到NSOperationQueue


3> 系统会自动为NSOperation对象封装的任务开启一条线程执行 或者 不加入队列调用-(void)start:在主线程执行


- (void)operationManage{
// 这样在主线程执行其实是画蛇添足的,只是为了做说明而写
NSBlockOperation *downloadImgPng = [NSBlockOperation blockOperationWithBlock:^{
//downloadImage 任务
NSLog(@"png -- 当前线程%@",[NSThread currentThread]);
}];
[downloadImgPng start];
}

//  NSInvocationOperation执行方式
- (void)operationManage{
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage) object:nil];
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
// 同一时间最多开启的线程数
operationQueue.maxConcurrentOperationCount = 6;
[operationQueue addOperation:invocationOperation];
// 取消所有队列中的任务
// [operationQueue cancelAllOperations];

// 取消执行的任务
// [invocationOperation cancel];
}
- (void)downloadImage{
}

//  NSBlockOperation执行方式
- (void)operationManage{
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
//downloadImage 任务
}];

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
// 同一时间最多开启的线程数
operationQueue.maxConcurrentOperationCount = 6;
[operationQueue addOperation:blockOperation];
}

- (void)operationManage{
/*
* 多任务队列添加依赖--串行执行
* 一般多任务队列默认是并行执行,添加依赖可按依赖条件顺序执行
*/
NSBlockOperation *downloadImgPng = [NSBlockOperation blockOperationWithBlock:^{
//downloadImage 任务
NSLog(@"png -- 当前线程%@",[NSThread currentThread]);
}];

NSBlockOperation *downloadImgJpg = [NSBlockOperation blockOperationWithBlock:^{
//downloadImage 任务
NSLog(@"jpg -- 当前线程%@",[NSThread currentThread]);
}];
[downloadImgJpg addDependency:downloadImgPng];

NSBlockOperation *downloadImgPdf = [NSBlockOperation blockOperationWithBlock:^{
//downloadImage 任务
NSLog(@"pdf -- 当前线程%@",[NSThread currentThread]);
}];
[downloadImgPdf addDependency:downloadImgJpg];

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
// 同一时间最多开启的线程数
operationQueue.maxConcurrentOperationCount = 6;
[operationQueue addOperations:@[downloadImgPng,downloadImgJpg,downloadImgPdf] waitUntilFinished:NO];
}

NSOperation对象执行状态监听




NSOperation对象执行状态.png
/*
此属性指定应用于添加到队列中的操作对象的服务级别。如果操作对象具有显式的服务水平集,则使用该值。此属性的默认值取决于您创建队列的方式。自己创建的队列,默认值是NSOperationQualityOfServiceBackground。为队列的mainqueue方法返回,默认值是nsoperationqualityofserviceuserinteractive和不能改变的。
服务级别影响给定操作对象访问系统资源的优先级,如CPU时间、网络资源、磁盘资源等。具有较高服务质量级别的操作在系统资源上被赋予更大的优先权,以便它们能更快地执行任务。您使用服务级别确保响应显式用户请求的操作优先于不重要的工作。
*/
@property NSQualityOfService qualityOfService;

typedef enum NSQualityOfService : NSInteger {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
} NSQualityOfService;


NSQualityOfServiceUserInteractive 用于直接提供交互式UI的工作。例如,处理控件事件或绘制到屏幕上。
NSQualityOfServiceUserInitiated 用于执行用户明确要求的工作,必须立即提交结果,以便进一步进行用户交互。例如,在用户在邮件列表中选择邮件后,加载电子邮件。
NSQualityOfServiceUtility 用于执行用户不太可能立即等待结果的工作。这项工作可能是用户要求的,也可能是自动启动的,并且经常使用非模态进度指示器在用户可见的时间尺度上运行。例如,周期性内容更新或大容量文件操作,如媒体导入。
NSQualityOfServiceBackground用于非用户发起或可见的工作。一般来说,用户不知道这项工作甚至正在发生。例如,预取内容,搜索索引、备份或同步与外部系统的数据。
NSQualityOfServiceDefault指示没有明确的服务质量信息。只要有可能,适当的服务质量由可用的来源决定。否则,选择的可能是NSQualityOfServiceUserInteractiveNSQualityOfServiceUtility之间服务水平的任意一种。



/*
此属性包含操作的相对优先级。这个值是用来影响其中的操作和执行顺序出列。
官方建议:为了确定优先级,应该始终使用这些常量(而不是定义的值)。
*/
@property NSOperationQueuePriority queuePriority;

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};

以上两个属性通过设置合适的值,能让资源利用更加合理。其他的API可查看官方文档使用,特别注意的是- (void)waitUntilFinished;这个接口要慎用,该接口绝不能在主线程调用,会产生死锁卡死主线程。一般来讲用- (void)addDependency:(NSOperation *)op;依赖API就够了,简单易用。


- (void)waitUntilFinished;接口的文档解释:



An operation object must never call this method on itself and should avoid calling it on any operations submitted to the same operation queue as itself. Doing so can cause the operation to deadlock. Instead, other parts of your app may call this method as needed to prevent other tasks from completing until the target operation object finishes. It is generally safe to call this method on an operation that is in a different operation queue, although it is still possible to create deadlocks if each operation waits on the other.
A typical use for this method would be to call it from the code that created the operation in the first place. After submitting the operation to a queue, you would call this method to wait until that operation finished executing.




翻译:操作对象绝不能自己调用这个方法,应该避免在提交给同一操作队列的任何操作中调用它。这样做可能导致操作死锁。相反,应用程序的其他部分可以根据需要调用此方法,以防止其他任务完成,直到目标操作对象完成为止。一般来说,在不同的操作队列中调用这种方法是安全的,但如果每个操作都等待另一个操作,仍然有可能造成死锁。
这种方法的一个典型用途是首先从创建操作的代码调用它。在向队列提交操作之后,您将调用此方法等待该操作完成执行。





waitUntilFinished简单使用.png
GCD的主要API使用

GCD的实现步骤

GCD实现多线程的步骤主要有2步:
1>创建队列
2>添加执行任务到队列中
也可以是1步,添加执行任务到系统标准队列
没错,就是这么简单易用!以下是代码片段:


// 创建队列 ,手动创建的队列需要做释放处理,因为Dispatch Queue并没有被作为OC对象处理
/*
* 1.创建串行队列
* 串行队列有两种创建方式
*/
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.gcd.mytest", NULL);
// 释放队列
dispatch_release(serialQueue);
dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.mytest", DISPATCH_QUEUE_SERIAL);
// 释放队列
dispatch_release(queue);
// 2.创建并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.gcd.mytest", DISPATCH_QUEUE_CONCURRENT);
// 3.添加同步任务到队列中
dispatch_sync(concurrentQueue, ^{
// 执行任务
});

// 4.添加异步任务到队列中
dispatch_async(concurrentQueue, ^{
// 执行任务
});

// 5.释放队列
dispatch_release(concurrentQueue);

// 一步搞定
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行操作
});

GCD的队列主要有两种Serial Dispatch QueueConcurrent Dispatch Queue。前者只有一个线程,后者根据任务量,可以开启多条线程。当然多个Serial Dispatch Queue是可以并行执行的。




两种队列和线程的关系.png

Main Dispatch Queue / Global Dispatch Queue

Main Dispatch QueueGlobal Dispatch Queue是系统提供的标准Dispatch Queue,这两个标准队列还有一个共同的优点,那就是相比于手动创建的队列,这两个队列不需要开发者做队列释放的操作,因为这两个队列对应用程序而言是全局的,详细可查看官方文档。


Main Dispatch Queue可能你已经猜到了,没错,它就是主线程队列,追加到Main Dispatch Queue的处理在主线程的RunLoop中执行,一些需要更新UI界面的操作可以放到这个线程中执行。


Global Dispatch QueueConcurrent Dispatch Queue类型的队列。所以我们一般是不用逐个生成Concurrent Dispatch Queue队列的,只要使用全局队列Global Dispatch Queue就可以了。


    // 主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

// 全局队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

创建全局队列的APIdispatch_get_global_queue(long identifier, unsigned long flags);需要传两个参数,第一个是优先级,根据实际处理内容选择合适的优先级。第二个官方文档称是为将来使用预留的,一般传数字0即可。
Global Dispatch Queue有4个执行优先级。


#define DISPATCH_QUEUE_PRIORITY_HIGH 2               // 高优先级
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认优先级
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低优先级
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台优先级


dispatch_sync / dispatch_asycn

dispatch_asycn是异步处理函数,该函数不会等待任务执行完,不会一直占用当前线程。


dispatch_sync是同步处理函数,在该函数中执行的任务不执行完,该函数会一直在当前线程等待。也可以说这个函数是简化版的dispatch_group_wait函数。


dispatch_sync要慎用,因为使用不当就会引起死锁。
比如在主线程调用:


    dispatch_queue_t mianQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^{
dispatch_sync(mainQueue, ^{
NSLog(@"Hello World");
});
});

Serial Diapatch Queue中调用也是一样


    dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.mytest", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
NSLog(@"Hello World");
});
});

死锁的原因很明显就是两个操作在同一个线程里互相等待对方执行完,一直互相等待,谁也不执行。



dispatch_set_target_queue

手动创建的队列可以通过dispatch_set_target_queue设定执行的优先级。将Dispatch Queue指定为dispatch_set_target_queue的函数参数,不仅可以变更Dispatch Queue的执行优先级,还可以作为执行阶层。


比如,将一个普通的Serial Diapatch Queue设定为与“后台优先级全局队列”一样的优先级和阶层,那么在执行时,它的执行优先级将高于其他普通的Serial Diapatch QueueConcunrrent Diapatch Queue,它的阶层也要比普通的Serial Diapatch QueueConcunrrent Diapatch Queue高,当它与“后台优先级全局队列”阶层一样时意味着,如果它在执行,其他普通的Serial Diapatch QueueConcunrrent Diapatch Queue都不能和它并行执行,必须等它执行完才能执行。


    // 创建串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.gcd.mytest", NULL);

// 创建并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.gcd.mytest", DISPATCH_QUEUE_CONCURRENT);

/*
* 参照serialQueue的优先级设置目标队列 即concurrentQueue的优先级
* 第一个参数为要设置优先级的queue,第二个参数是参照物,既将第一个queue的优先级和第二个queue的优先级设置一样。
*/
dispatch_set_target_queue(concurrentQueue, serialQueue);


dispatch_after

dispatch_after用于延时处理,需要注意的是并不是dispatch_after在延时指定时间后执行,而是在指定时间把任务添加到队列中,相当于加了一个计时器,时间到了就把任务添加到队列中了。


栗子:
延时3秒打印Hello Word


    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull*NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{NSLog(@"Hello World");});

dispatch_time的第一个参数是起始时间可传:DISPATCH_TIME_NOWDISPATCH_TIME_FOREVER
DISPATCH_TIME_NOW : 表示从现在开始 DISPATCH_TIME_FOREVER:表示持续等待,稍后会用到。
第二个参数是秩序多久,"ull"是C语言的数值字面量,是显示表明类型时使用的字符串(表示"unsight long long")为了精确时间写上"ull"NSEC_PER_SEC是秒时间单位的一种,表示纳秒级精确的一秒。


#define NSEC_PER_SEC 1000000000ull  // 每秒有多少纳秒
#define NSEC_PER_MSEC 1000000ull // 每毫秒有多少纳秒
#define USEC_PER_SEC 1000000ull // 每秒有多少毫秒
#define NSEC_PER_USEC 1000ull // 每微秒有多少毫秒


Dispatch Group

有时我们想等添加到队列中的所有任务都执行完再执行结束处理。Serial Dispatch Queue好说,本来就是串行的。但是Concurrent Dispatch Queue就不行了,异步的我们根本不知道哪个是最后执行完的。这个时候就可以用到Dispatch Group了。


    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{NSLog(@"block 0");});
dispatch_group_async(group, queue, ^{NSLog(@"block 1");});
dispatch_group_async(group, queue, ^{NSLog(@"block 2");});
/*
* 无论向什么样的Dispatch Queue中添加任务,使用Dispatch Group都可以监听这个任务的执行结束
* 所有任务结束时,会执行dispatch_group_notify函数的block
*/
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done");});
dispatch_release(group);

dispatch_group_wait也可以达到同样的效果,不过前者更简洁,所以建议用dispatch_group_notify


    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{NSLog(@"block 0");});
dispatch_group_async(group, queue, ^{NSLog(@"block 1");});
dispatch_group_async(group, queue, ^{NSLog(@"block 2");});

dispatch_time_t time = dispatch_time(DISPATCH_TIME_FOREVER, 1ull*NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
// group的全部任务执行完毕
}
else {
// group的某个任务还在执行中
}
dispatch_release(group);


dispatch_once

dispatch_once大家比较熟,因为线程安全的单例模式常用到它。


+(instancetype)sharedSingleton{
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}

还可以用@synchronized()这个互斥锁实现单例模式:


+(instancetype)sharedSingleton{
static id instance = nil;
@synchronized (self) {
if (!instance) {
instance = [[self alloc] init];
}
}
return instance;
}


dispatch_barrier_async

在访问数据库或文件时,为避免数据竞争,前面讲到可以使用Serial Dispatch Queue。但是其实如果是读取和读取并行执行是不会引起数据竞争的,如果能把这部分操作拆分出来,那无疑会提高访问效率。dispatch_barrier_async配合手动创建的Concurrent Dispatch Queue就可以帮我们做到。在执行dispatch_barrier_async时,它会等正在执行的任务执行完开始,当它结束后其他任务才会再开始执行。在SDWebImage框架中也有用到它和它的同步函数dispatch_barrier_sync


dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.mytest", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 执行读取操作
});
dispatch_async(queue, ^{
// 执行读取操作
});
dispatch_barrier_async(queue, ^{
// 执行写入操作
});
dispatch_async(queue, ^{
// 执行读取操作
});
dispatch_async(queue, ^{
// 执行读取操作
});


dispatch_suspend / dospatch_resume

在队列大量任务执行时,需要临时挂起队列时调用dispatch_suspend
dispatch_suspend(queue)挂起队列
dospatch_resume (queue)恢复队列


GCD还提供可多线程读取同一个大型文件的APIdispatch I/Odispatch Data,还有其他很多有意思有用的API,大家尽可以去查看官方文档学习和使用。


参考
苹果官方文档
《现代操作系统》
《Objective-C高级编程- iOS与OS X多线程和内存管理》







最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台