Objecitve-C:GCD 的使用

参考资料:《Objective-C 高级编程:iOS与OS X多线程和内存管理》

1. GCD 概要

GCD(Grand Central Dispatch) 是异步执行任务的技术之一。是一种与 Block 有关的技术,它提供了对线程的抽象,这种抽象基于“派发队列”(Dispatch Queue)。开发者可将 Block 排入队列中,由 GCD 负责处理所有调度事宜。GCD 会根据系统资源情况,适时地创建、复用、摧毁后台线程,以便处理每个队列。

开发者只需要定义想执行的任务并追加到恰当的Dispatch Queue中,GCD 就能生成必要的线程并计划执行任务。

dispatch_async( queue, ^{
  // 长时间处理任务
  // ...
  // 长时间处理结束,主线程使用该处理结果
  dispatch_async( dispatch_get_main_queue(), ^{
    // 主线程执行的处理
  })
})

2. GCD 的 API

2.1 Dispatch Queue

即执行处理的等待队列。开发者通过 dispatch_async 等API,在 Block 语法中记述想执行的处理并将其追加到 Dispatch Queue 中,Dispatch Queue 按照追加的顺序(FIFO)进行处理。

存在两种 Dispatch Queue:

  1. Serial Dispatch Queue 串行队列

使用一个线程,会等待当前执行的任务结束后才会派发处理下一个任务,按照添加到队列的顺序进行派发。

  1. Concurrent Dispatch Queue 并行队列

同时使用多个线程,不等待当前执行的任务结束,如果当前的线程数足够,就会派发任务到多个线程同时执行。

并行执行的处理数量取决于当前系统的状态。由系统决定应当使用的线程数,并只生成所需的线程进行处理。当处理结束时,系统会结束不再需要的线程。

在并行队列中执行处理时,执行顺序会根据处理内容和系统状态发生改变,而串行队列中任务的执行顺序是固定的。

2.1.1 创建 Dispatch Queue

使用 dispatch_queue_create 生成派发队列。

dispatch_queue_t mySerialDispatchQueue = 
    dispatch_queue_create("com.example.gcd.mySerialDispatchQueue", NULL);
// 第一个参数为线程的名称,
// 第二参数为 NULL 时,生成 Serial Dispatch Queue;指定为 DISPATCH_QUEUE_CONCURRENT 时,生成 Concurrent Dispatch Queue。
应限制串行队列的数量

dispatch_queue_create 函数可生成任意多个派发队列。

当生成多个 Serial Dispatch Queue 时,各个 Serial Dispatch Queue 将并行执行。系统对于一个 Serial Dispatch Queue 只生成并使用一个线程。

如果过多使用多线程,引起大量的上下文切换(CPU 寄存器等信息会保存到各自路径专用的内存块中),会消耗大量内存,大幅度降低系统的响应性能。

所以,只在为了避免资源竞争时使用 Serial Dispatch Queue

当想并行执行不会发生数据竞争等问题的处理时,使用 Concurrent Dispatch Queue。对于 Concurrent Dispatch Queue,不管生成多少,由于系统只使用有效管理的线程,因此不会发生 Serial Dispatch Queue 的那些问题。

Dispatch Queue 的内存管理类似 ARC,也通过引用计数进行管理,通过 dispatch_retain 函数和dispatch_release 函数来管理引用计数。

在 iOS 6.0 or Mac OS X 10.8 后,ARC 已经能够管理GCD 对象了。不再需要手动释放。

dispatch_queue_t myDispatchQueue = 
    dispatch_queue_create("com.example.gcd.myDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(myDispatchQueue, ^{NSLog(@" ")};);

2.1.2 系统提供的派发队列

也可以不用特意生成,使用系统提供的派发队列。

Main Dispatch Queue :是在主线程中执行的Serial DIspatch Queue,只有一个,添加到这个线程的任务都会在主线程的 RunLoop 中执行。

Global Dispatch Queue :所有应用程序都能使用的 Concurrent Dispatch Queue,因此没有必要通过 dispatch_queue_create 函数生成并行队列,只要获取 Global Dispatch Queue 使用即可。

Global Dispatch Queue 有 4 个执行优先级(High, Default, Low, Background),但此执行优先级只能进行大致的区分。

获取方法
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
dispatch_queue_t globalDIspatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
// 第二个参数为保留字段

2.2 Dispatch Group

Dispatch Group 可以使 Concurrent Dispatch Queue 中的多个任务全部结束后在指定队列上执行 Block。

        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(@"blk0");});
        dispatch_group_async(group, queue, ^{NSLog(@"blk1");});
        dispatch_group_async(group, queue, ^{NSLog(@"blk2");});
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"done");
        });

使用 DIspatch Group 都可以监视这些任务执行的结束。一旦检测到所有任务执行结束,就可将结束的处理异步派发到 指定队列 中。

可以使用 dispatch_group_wait 函数等待全部处理结束,该函数会堵塞当前线程,直到超过了指定的时间或全部任务执行结束。

long result = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// 若全部处理执行结束则返回 0;
// 若经过了指定时间但 group 中某一处理仍未结束,则返回值不为 0。
  • 使用 dispatch_group_wait 等待 dispatch group 执行完毕,会阻塞线程;
  • 使用 dispatch_group_notify 可以向此函数传入块,等 dispatch group 执行完毕后,块会在特定线程上执行,不会阻塞当前线程。

2.3 dispatch_barrier_async

主要用于实现并行读串行写的功能,使用 Concurrent Dispatch Queue 和 dispatch_barrier_async 函数可实现高效率的数据库访问和文件访问。

使用:

       dispatch_queue_t queue = dispatch_queue_create("com.barrierqueue", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(queue, ^{NSLog(@"reading1");});
        dispatch_async(queue, ^{NSLog(@"reading2");});
        dispatch_async(queue, ^{NSLog(@"reading3");});
        dispatch_barrier_async(queue, ^{NSLog(@"writing");});
        dispatch_async(queue, ^{NSLog(@"reading4");});
        dispatch_async(queue, ^{NSLog(@"reading5");});
        dispatch_async(queue, ^{NSLog(@"reading6");});

dispatch_queue_create 生成 Concurrent Dispatch Queue ,然后用 dispatch_barrier_async 函数代替 dispatch_async 函数即可。

在 dispatch_get_global_queue 获取的队列上使用没有效果。

由 dispatch_barrier_async 添加的任务会等 Concurrent Dispatch Queue 上的并行执行的任务全部结束后,再将指定的任务追加到该 Concurrent Dispatch Queue 中。然后等待 dispatch_barrier_async 添加的任务执行完毕后,Concurrent Dispatch Queue 才恢复为并行执行。

2.4 dispatch_sync

dispatch_sync 会将指定 Block 同步添加到队列中,在添加的 Block 执行结束前,dispatch_sync 函数会一直堵塞直到执行结束。

dispatch_async 将任务添加到队列后会继续往下执行。

在同步队列中使用 dispatch_sync 添加任务会导致死锁:

    dispatch_queue_t queue = dispatch_queue_create("queue", NULL);
    dispatch_async(queue, ^{
        dispatch_sync(queue, ^{
            NSLog(@"deadLock");
        });
    });

另外,dispatch_barrier_async 也有 sync 版本,功能类似,但会等待添加的 Block 执行结束再执行下面的代码。

2.5 Dispatch Semaphore

使用 Dispatch Semaphore 可以进行更细粒度的排他控制。

DIspatch Semaphore 是持有计数的信号。如果信号量为 0 时则等待,信号量大于等于 1 则开始执行。
每当有线程进入“加锁代码”后就调用信号等待命令将计数减 1,此时计数为 0,其他线程无法进入,执行完后发送信号通知将信号量加 1,其他线程开始进入执行,如此一来就达到了线程同步目的。

通过 dispatch_semaphore_carete(1) 生成信号量为 1 的 DIspatch Semaphore
信号量的初始值,可以用来控制线程并发访问的最大数量。
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步。

dispatch_semaphore_wait 函数类似 dispatch_group_wait ,会堵塞线程等待,当计数值大于等于 1 时,对该计数值减 1 并从函数返回,继续执行后续代码。

dispatch_semaphore_signal(semaphore) 函数会将计数值加 1,如果有通过 dispatch_semaphore_wait 函数等待的线程,就由最先等待的线程执行。

可以通过 dispatch_semaphore_wait 的返回值进行分支处理:

    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1null * NSEC_PER_SEC);
    long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    if (result == 0) {
        // 信号量计数值大于等于 1,执行需要排他控制的处理
    } else {
        // 信号量计数值为 0
    }

使用示例:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSMutableArray *array = [[NSMutableArray alloc] init];
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    for (int i = 0; i < 10000; i++) {
        dispatch_async(queue, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            
            [array addObject:[NSNumber numberWithInt:i]];
            
            dispatch_semaphore_signal(semaphore);
            NSLog(@"%@",array.lastObject);
        });
    }

在没有 Serial Dispatch Queue 和 dispatch_barrier_async 函数那么大粒度且一部分处理需要进行排他控制的情况下,便可使用 Dispatch Semaphore。

2.6 dispatch_apply

该函数可以按指定的次数将指定的 Block 追加到指定的 Dispatch Queue 中,并等待全部处理执行结束。

dispatch_apply 可以实现类似 Dispatch Group 的效果,但和 dispatch_sync 函数一样,会堵塞当前线程直到执行结束。所以假如把 Block 派给了当前队列,就将导致死锁。若想在后台执行任务,则应使用 Dispatch Group。

推荐在 dispatch_async 函数中异步执行 dispatch_apply 函数。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_async(queue, ^{
        dispatch_apply(10, queue, ^(size_t index) {
            NSLog(@"%zu", index);
        });
    });

2.7 Dispatch Source

Dispatch SourceBSD系内核惯有功能kqueue的包装。kqueue 是在XUN内核中发生各种事件时,在应用程序编程方执行处理的技术。
其 CPU 负荷非常小,尽量不占用资源。kqueue 可以说是应用程序处理XUN内核中发生的各种事件的方法中最优秀的一种。

当事件发生时,Dispatch Source 会将 Block 添加到到指定的队列中去执行。和手工提交到队列的任务不同,Dispatch Source 为应用提供连续的事件源。除非显式地取消,否则 Dispatch Source 会一直保留与队列的关联。
只要相应的事件发生,就会提交关联的任务到队列中执行。

为了防止事件积压到队列,Dispatch Source 实现了事件合并机制。
如果新事件在上一个事件处理器出列并执行之前到达,Dispatch Source 会将新旧事件的数据合并。
根据事件类型的不同,合并操作可能会替换旧事件,或者更新旧事件的信息。

当配置一个Dispatch Source时,需要指定监听的事件、处理事件的队列、以及处理事件的 Block。

2.7.1 可以处理的事件种类

宏定义内容
DISPATCH_SOURCE_TYPE_DATA_ADD变量增加
DISPATCH_SOURCE_TYPE_DATA_OR变量OR
DISPATCH_SOURCE_TYPE_MACH_SENDMACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECVMACH端口接收
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE内存报警
DISPATCH_SOURCE_TYPE_PROC进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号
DISPATCH_SOURCE_TYPE_READIO读操作,如对文件的操作、socket操作的读响应
DISPATCH_SOURCE_TYPE_SIGNAL接收到UNIX信号时响应
DISPATCH_SOURCE_TYPE_TIMER定时器
DISPATCH_SOURCE_TYPE_VNODE文件状态监听,文件被删除、移动、重命名
DISPATCH_SOURCE_TYPE_WRITEIO写操作,如对文件的操作、socket操作的写响应

2.7.2 使用

dispatch source 必须进行额外的配置才能被使用,dispatch_source_create 函数返回的 dispatch source 将处于挂起状态。此时 dispatch source 会接收事件,但是不会进行处理。

  • dispatch_source_set_event_handler 设置事件处理器
  • dispatch_source_set_cancel_handler 取消处理器
  • 使用 dispatch_suspend / dispatch_resume 临时地挂起和继续 dispatch source 的事件递送。

dispatch source 挂起期间,发生的事件会被累积,直到 dispatch source
继续。但是不会递送所有事件,而是先合并到单一事件,然后再一次递送。比如监控一个文件的文件名变化,就只会递送最后一次的变化事件。

计时器示例
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        
        // 定时器设定为 5s 后开始触发,2s 触发一次,允许延迟 1s。
        dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC);
        uint64_t interval = 2ull * NSEC_PER_SEC;
        uint64_t leeway = 1ull * NSEC_PER_SEC;
        dispatch_source_set_timer(timer, start, interval, leeway);
        
        // 触发时执行的 Block
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"wakeup");
        });
        
        // 取消时执行的 Block
        dispatch_source_set_cancel_handler(timer, ^{
            NSLog(@"canceled");
        });
        
        dispatch_resume(timer);

2.7.3 优点

Dispatch Queue 中没有取消这一概念,一旦将处理追加到 Dispatch Queue 后就没有办法可以将该处理去除,也没有办法可以在执行中取消该处理。(可以使用 NSOperationQueue 等的其他方法)

Dispatch Source 是可以取消的。而且取消时需要执行的处理可指定为回调用的 Block。

使用 Dispatch Source 实现 XNU 内核中发生的事件处理要比直接使用 kqueue 实现简单。

2.8 dispatch_suspend / dispatch_resume

可以挂起 / 恢复指定的 Dispatch Queue。这些函数对已经执行的处理没有影响。

  • dispatch_suspend 会使队列中尚未执行的任务停止执行。
  • dispatch_resume 则使得这些任务继续执行。

2.9 dispatch_set_target_queue

用于变更使用dispatch_queue_create生成的派发队列的执行优先级。

dispatch_queue_create 函数生成的派发队列使用 default 优先级的线程。

使用这个函数还可以构建 派发队列 的执行阶层。在多个 Serial Dispatch Queue 中用 dispatch_set_target_queue 指定目标为某一个 Serial Dispatch Queue,可以防止处理并行执行。

2.10 dispatch_after

在指定时间后执行处理可用 dispatch_after 来实现。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
        dispatch_after(time, dispatch_get_main_queue(), ^{
            NSLog(@"wait at least 3 seconds.")
        });

dispatch_after 函数并不是在指定时间后执行处理,而只是在指定时间后追加处理到指定队列。以上代码与 3 秒后用 dispatch_async 函数添加 Block 到队列 效果相同。

  • dispatch_time 得到相对时间;
  • 可以使用 dispatch_walltimestruct timespec 得到绝对时间。

2.11 dispatch_once

该函数可以保证程序中只执行一次指定任务,可以在多线程下保证安全。

实现单例模式:

+ (id) sharedInstance {
    static XXObject *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

dispatch_once_t 是 int 类型的指针,对于只需执行一次的 Block,每次调用函数传入的标记必须完全相同,因此通常将标记变量声明在 static 或 global 作用域里。

2.12 Dispatch I/O

使用 DIspatch I/O 和 Dispatch Data 将大文件分块进行并行读取处理。
如果想提高文件读取速度,可以尝试 Dispatch I/O。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值