您的位置:首页 > 其它

浅谈GCD(Grand Central Dispatch)

2015-08-22 22:24 369 查看
一. iOS的多线程简述

1. 分类
(1) NSThread 每个NSThread对象对应一个线程,量级较轻(真正的多线程)
(2) 以下两点是苹果专门开发的“并发”技术,使得程序员可以不再去关心线程的具体使用问题
 NSOperation/NSOperationQueue 面向对象的线程技术
 GCD —— Grand Central Dispatch(派发) 是基于C语言的框架,可以充分利用多核,是苹果推荐使用的多线程技术 
以上三种编程方式从上到下,抽象度层次是从低到高的,抽象度越高的使用越简单

2. 特点
(1) NSThread
 –优点:NSThread 比其他两个轻量级,使用简单
 –缺点:需要自己管理线程的生命周期、线程同步、加锁、睡眠以及唤醒等。线程同步对数据的加锁会有一定的系统开销
(2) NSOperation
 –不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上
 –NSOperation是面向对象的
(3) GCD
 –Grand Central Dispatch是由苹果开发的一个多核编程的解决方案。iOS4.0+才能使用,是替代NSThread, NSOperation的高效和强大的技术
 –GCD是基于C语言的

二. GCD简介

1. GCD是什么?
   GCD是一套低层API,提供了一种新的方法来进行并发程序编写。从基本功能上讲,GCD有点像NSOperationQueue,他们都允许程序将任务切分为多个单一任务然后提交至工作队列来并发地或者串行地执行。GCD比之NSOpertionQueue更底层更高效,并且它不是Cocoa框架的一部分。
    除了代码的平行执行能力,GCD还提供高度集成的事件控制系统。可以设置句柄来响应文件描述符、mach ports(Mach port 用于 OS X上的进程间通讯)、进程、计时器、信号、用户生成事件。这些句柄通过GCD来并发执行。
    GCD的API很大程度上基于block,当然,GCD也可以脱离block来使用,比如使用传统c机制提供函数指针和上下文指针。实践证明,当配合block使用时,GCD非常简单易用且能发挥其最大能力。

2. 为什么使用GCD
 (1) 易用: GCD比之thread跟简单易用。由于GCD基于work unit而非像thread那样基于运算,所以GCD可以控制诸如等待任务结束、监视文件描述符、周期执行代码以及工作挂起等任务。基于 block的血统导致它能极为简单得在不同代码作用域之间传递上下文。
 (2) 效率: GCD被实现得如此轻量和优雅,使得它在很多地方比之专门创建消耗资源的线程更实用且快速。这关系到易用性:导致GCD易用的原因有一部分在于你可以不用担心太多的效率问题而仅仅使用它就行了。
 (3) 性能: GCD自动根据系统负载来增减线程数量,这就减少了上下文切换以及增加了计算效率。

三. GCD详解

1.Dispatch Object
   尽管GCD是纯c语言的,但它被组建成面向对象的风格。GCD对象被称为dispatch object。Dispatch object像Cocoa对象一样是引用计数的。使用dispatch_release和dispatch_retain函数来操作dispatch
object的引用计数来进行内存管理。但不像Cocoa对象,dispatch object并不参与垃圾回收系统,所以即使开启了GC,你也必须手动管理GCD对象的内存。

2. Dispatch queue
   GCD的基本概念就是dispatch queue。dispatch queue是一个对象,它可以接受任务,并将任务以先到先执行的顺序来执行。dispatch
queue可以是并发的或串行的。并发任务会像NSOperationQueue那样基于系统负载来合适地并发进行,串行队列同一时间只执行单一任务。 
    GCD中有三种队列类型:
    Global queues: 全局队列是并发队列,并由整个进程共享。进程中存在三个全局队列:高、中(默认)、低三个优先级队列。可以调用dispatch_get_global_queue函数传入优先级来访问队
列。
    The main queue: 与主线程功能相同。实际上,提交至main queue的任务会在主线程中执行。main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关
的,所以这是一个串行队列。
    用户队列: 用户队列 (GCD并不这样称呼这种队列, 但是没有一个特定的名字来形容这种队列,所以我们称其为用户队列) 是用函数 dispatch_queue_create 创建的队列. 这些队列是串行的。正
因为如此,它们可以用来完成同步机制, 有点像传统线程中的mutex。
    Dispatch Queues的生成方式:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //获得程序进程缺省产生的并发队列,可设定优先级来选择高、中、低三个优先级队列。由于是系统默认生成的,所以无法调用dispatch_resume()和dispatch_suspend()来控制执行继续或中断。需要注意的是,三个队列不代表三个线程,可能会有更多的线程。并发队列可以根据实际情况来自动产生合理的线程数,也可理解为dispatch队列实现了一个线程池的管理,对于程序逻辑是透明的。
dispatch_queue_t queue = dispatch_get_main_queue();//获得主线程的dispatch队列, 同样无法控制主线程dispatch队列的执行继续或中断    dispatch_queue_t queue = dispatch_queue_create("com.dispatch.serial", DISPATCH_QUEUE_SERIAL); //生成一个串行队列,队列中的block按照先进先出(FIFO)的顺序去执行,实际上为单线程执行。第一个参数是队列的名称,在调试程序时会非常有用,所有尽量不要重名了。  
dispatch_queue_t queue = dispatch_queue_create("com.dispatch.concurrent", DISPATCH_QUEUE_CONCURRENT); //生成一个并发执行队列,block被分发到多个线程去执行 


    我们可以使用dispatch_async或dispatch_sync函数来加载需要运行的block。 

dispatch_async(queue, ^{
//block具体代码
}); //异步执行block,函数立即返回
dispatch_sync(queue, ^{
//block具体代码
}); //同步执行block,函数等到block执行完毕。编译器会根据实际情况优化代码,所以有时候你会发现block其实还在当前线程上执行,并没用产生新线程。


    实际运用中,常见的网络请求数据多线程执行模型:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//子线程中开始网络请求数据
//更新数据模型
dispatch_sync(dispatch_get_main_queue(), ^{
//在主线程中更新UI代码
});
});
    dispatch队列是线程安全的,可以利用串行队列实现锁的功能。比如多线程写同一数据库,需要保持写入的顺序和每次写入的完整性 

dispatch_queue_t queue = dispatch_queue_create("com.dispatch.writedb", DISPATCH_QUEUE_SERIAL); //注意要创建串行队列
- (void)writeDB:(NSData *)data
{
dispatch_async(queue1, ^{
//write database
});
}


    下一次调用writeDB:必须等到上次调用完成后才能进行,保证writeDB:方法是线程安全的。
    dispatch队列还实现其它一些常用函数,包括:

void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block) //block在程序运行中止运行一次,可以用来实现单例
void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t)); //重复执行block,需要注意的是这个方法是同步返回,也就是说等到所有block执行完毕才返回,如需异步返回则嵌套在dispatch_async中来使用。多个block的运行是否并发或串行执行也依赖queue的是否并发或串行。
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block); //这个函数可以设置同步执行的block,它会等到在它加入队列之前的block执行完毕后,才开始执行。在它之后加入队列的block,则等到这个block执行完毕后才开始执行。
void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block); //同上,除了它是同步返回函数
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block); //延迟执行block
void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue); //它会把需要执行的任务对象指定到不同的队列中去处理,这个任务对象可以是dispatch队列,也可以是dispatch源(以后博文会介绍)。而且这个过程可以是动态的,可以实现队列的动态调度管理等等


3. Dispatch group
    (1) void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block)
将多个block组成一组以监测这些Block全部完成或者等待全部完成时发出的消息

dispatch_queue_t queue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_t group=dispatch_group_create();
dispatch_group_async(group, queue, ^{NSThread sleepForTimeInterval:4];NSLog(@"blk0");});
dispatch_group_async(group, queue, ^{[NSThread sleepForTimeInterval:2];NSLog(@"blk1");});
dispatch_group_async(group, queue, ^{[NSThread sleepForTimeInterval:3];NSLog(@"blk2");});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done");});
NSLog(@"HELLO”);


output:HELLO blk1 blk2 blk0 done

    (2) dispatch_group_wait(group, DISPATCH_TIME_FOREVER); 
等待group关联的block执行完毕,也可以设置超时参数,如果dispatch_group_wait函数的返回值不为零,说明group内处理为结束,否则为零

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{[NSThread sleepForTimeInterval:4];NSLog(@"blk0");});
dispatch_group_async(group, queue, ^{[NSThread sleepForTimeInterval:2];NSLog(@"blk1");});
dispatch_group_async(group, queue, ^{[NSThread sleepForTimeInterval:3];NSLog(@"blk2");});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done");});
int result=dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC));
if (result==0)
{
NSLog(@"the group has finished");
}
else
{
NSLog(@"the group is processing");
}
NSLog(@"hello");


4. Dispatch semaphore
    信号量,GCD通过此操作来控制并发,
    首先,简单介绍一下信号量,信号量通过PV原语来限制线程对资源的访问
    信号量S是一个整数,S大于等于零是代表可供并发进程使用的资源实体数,当S小于零时则表示正在等待使用临界区的进程数。
    P原语操作的动作是:
       S减1;
       若S减1后仍大于或等于零,则进程继续执行;
       若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转进程调度。
    V原语操作的动作是:
       S加1;
       若相加结果大于零,则进程继续执行;
       若相加结果小于或等于零,则从该信号的等待队列中唤醒一等待进程,然后再返回原进程继续执行或转进程调度。
    PV操作对于每一个进程来说,都只能进行一次,而且必须成对使用。在PV原语执行期间不允许有中断的发生。
    其次,介绍一下Dispatch semaphore,Dispatch semaphore通过一下三个函数来实现信号量操作

dispatch_semaphore_t dispatch_semaphore_create(long value); //创建一个信号量
long dispatch_semaphore_signal(dispatch_semaphore_t dsema); //提高信号量
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); //等待降低信号量
    代码实例:

- (void)task_test
{
_semaphore = dispatch_semaphore_create(1);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
[self task_first];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
[self task_second];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
[self task_third];
});
}

- (void)task_first
{
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%@",@"First task starting");
sleep(1);
NSLog(@"%@", @"First task is done");
dispatch_semaphore_signal(_semaphore);
}

- (void)task_second
{
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%@",@"Second task starting");
sleep(1);
NSLog(@"%@", @"Second task is done");
dispatch_semaphore_signal(_semaphore);
}

- (void)task_third
{
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%@",@"Thrid task starting");
sleep(1);
NSLog(@"%@", @"Thrid task is done");
dispatch_semaphore_signal(_semaphore);
}


    大家可以将dispatch_semaphore_create(1)的参数调成1、2、3,看看输出结果是什么

5. Dispatch sources
    dispatch source是一个监视某些类型事件的对象。当这些事件发生时,它自动将一个block放入一个dispatch queue的执行例程中   
下面是GCD 10.6.0版本支持的事件:
        Mach port send right state changes.
        Mach port receive righ
b64e
t
state changes.
        External process state
change.
        File descriptor ready
for read.
        File descriptor ready
for write.
        Filesystem node event.
        POSIX signal.
        Custom timer.
        Custom event.
    (1)用户事件
    这种事件是由你调用dispatch_source_merge_data函数来向自己发出的信号这个名字对于一个发出事件信号的函数来说,太怪异了。这个名字的来由是GCD会在事件句柄被执行之前自动将多个事件进行联结。你可以将数据“拼接”至dispatch
source中任意次,并且如果dispatch queue在这期间繁忙的话,GCD只会调用该句柄一次

用户事件有两种: DISPATCH_SOURCE_TYPE_DATA_ADD 和 DISPATCH_SOURCE_TYPE_DATA_OR.用户事件源有个 unsigned long data属性,我们将一个 unsigned long传入 dispatch_source_merge_data。当使用 _ADD版本时,事件在联结时会把这些数字相加。当使用 _OR版本时,事件在联结时会把这些数字逻辑与运算。当事件句柄执行时,我们可以使用dispatch_source_get_data函数访问当前值,然后这个值会被重置为0。

让我假设一种情况。假设一些异步执行的代码会更新一个进度条。因为主线程只不过是GCD的另一个dispatch queue而已,所以我们可以将GUI更新工作push到主线程中。然而,这些事件可能会有一大堆,我们不想对GUI进行频繁而累赘的更新,理想的情况是当主线程繁忙时将所有的改变联结起来。

用dispatch source就完美了,使用DISPATCH_SOURCE_TYPE_DATA_ADD,我们可以将工作拼接起来,然后主线程可以知道从上一次处理完事件到现在一共发生了多少改变,然后将这一整段改变一次更新至进度条。

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(source, ^{
[progressIndicator incrementBy:dispatch_source_get_data(source)];
});
dispatch_resume(source);

dispatch_apply([array count], globalQueue, ^(size_t index) {
// do some work on data at index
dispatch_source_merge_data(source, 1);
});


数据会被并发处理。当每一段数据完成后,会通知dispatch source并将dispatch source data加1,这样我们就认为一个单元的工作完成了。事件句柄根据已完成的工作单元来更新进度条。若主线程比较空闲并且这些工作单元进行的比较慢,那么事件句柄会在每个工作单元完成的时候被调用,实时更新。如果主线程忙于其他工作,或者工作单元完成速度很快,那么完成事件会被联结起来,导致进度条只在主线程变得可用时才被更新,并且一次将积累的改变更新至GUI。

(2)内建事件
    用GCD读取标准输入

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t stdinSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, STDIN_FILENO, 0, globalQueue);
dispatch_source_set_event_handler(stdinSource, ^{
char buf[1024];
int len = read(STDIN_FILENO, buf, sizeof(buf));
if(len > 0)
NSLog(@"Got data from stdin: %.*s", len, buf);
});
dispatch_resume(stdinSource);


对于标准输入,这没什么问题,但是对于其他文件描述符,我们必须考虑在完成读写之后怎样清除描述符。对于dispatch source还处于活跃状态时,我们决不能关闭描述符。如果另一个文件描述符被创建了(可能是另一个线程创建的)并且新的描述符刚好被分配了相同的数字,那么你的dispatch source可能会在不应该的时候突然进入读写状态。这个bug可不是什么好玩的事儿。

适当的清除方式是使用 dispatch_source_set_cancel_handler,并传入一个block来关闭文件描述符。然后我们使用 dispatch_source_cancel来取消dispatch source,使得句柄被调用,然后文件描述符被关闭。

使用其他dispatch source类型也差不多。总的来说,你提供一个source(mach port、文件描述符、进程ID等等)的区分符来作为diapatch source的句柄。mask参数通常不会被使用,但是对于 DISPATCH_SOURCE_TYPE_PROC 来说mask指的是我们想要接受哪一种进程事件。然后我们提供一个句柄,然后恢复这个source(前面我加粗字体所说的,得先恢复),搞定。dispatch
source也提供一个特定于source的data,我们使用 dispatch_source_get_data函数来访问它。例如,文件描述符会给出大致可用的字节数。进程source会给出上次调用之后发生的事件的mask。

(3)计时器

计时器事件稍有不同。它们不使用handle/mask参数,计时器事件使用另外一个函数 dispatch_source_set_timer 来配置计时器。这个函数使用三个参数来控制计时器触发:

 start参数控制计时器第一次触发的时刻。参数类型是 dispatch_time_t,这是一个opaque类型,我们不能直接操作它。我们得需要dispatch_time 和  dispatch_walltime 函数来创建它们。另外,常量  DISPATCH_TIME_NOW 和DISPATCH_TIME_FOREVER 通常很有用。

 interval参数没什么好解释的。

 leeway参数比较有意思。这个参数告诉系统我们需要计时器触发的精准程度。所有的计时器都不会保证100%精准,这个参数用来告诉系统你希望系统保证精准的努力程度。如果你希望一个计时器没五秒触发一次,并且越准越好,那么你传递0为参数。另外,如果是一个周期性任务,比如检查email,那么你会希望每十分钟检查一次,但是不用那么精准。所以你可以传入60,告诉系统60秒的误差是可接受的。

这样有什么意义呢?简单来说,就是降低资源消耗。如果系统可以让cpu休息足够长的时间,并在每次醒来的时候执行一个任务集合,而不是不断的醒来睡去以执行任务,那么系统会更高效。如果传入一个比较大的leeway给你的计时器,意味着你允许系统拖延你的计时器来将计时器任务与其他任务联合起来一起执行。

  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: