ITKeyword,专注技术干货聚合推荐

注册 | 登录

GCD 多线程API编程笔记

HeYingShu 分享于 2017-04-21

推荐:【iOS沉思录】NSThread、GCD、NSOperation多线程编程总结

OC中的多线程 OC中多线程根据封装程度可以分为三个层次:NSThread、GCD和NSOperation,另外由于OC兼容C语言,因此仍然可以使用C语言的POSIX接口来实现多线程,只

2020腾讯云共同战“疫”,助力复工(优惠前所未有!4核8G,5M带宽 1684元/3年),
地址https://cloud.tencent.com/act/cps/redirect?redirect=1053

2020阿里云最低价产品入口,含代金券(新老用户有优惠),
地址https://www.aliyun.com/minisite/goods

GCD 是 iOS 编程中实现多线程的常用 API,使用方便,无须手动进行线程的管理,由系统代劳。同时GCD基于C实现,性能强。

在名著《Pro multithreading and memory management for iOS and OS X》(中文名:《Objective-C 高级编程 iOS与 OSX多线程和内存管理》)中,专门有章节讲解,特根据此书讲解做些笔记。

一 概要

GCD是Grand Central Dispatch(GCD)的缩写,是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现。开发者只需定义想执行的任务,然后加入适当的 Dispatch Queue 中,GCD 就能生成相应的线程并计划执行任务

由于线程管理是系统来实现的,因此可以统一管理,也可执行任务,这样就比之前的线程更有效率。

二 API 详解

1. Dispatch Queue

“Dispatch Queue” 是执行处理的等待队列。应用程序编程通过 dispatch_async函数等 API,在 Block 语法中记述想执行的处理,并将其追加到 Dispatch Queue 中。 Dispatch Queue 按照追加的顺序 (先进先出,即FIFO,First In Firs Out)执行处理。

根据处理的策略,分为两种Dispatch Queue, 一种是等待现在执行中处理的Serial Dispatch Queue;另外一种是不等待现在执行任务队列处理的 Concurrent Dispatch Queue。
比较 Dispatch Queue 的种类如下:

                       Dispatch Queue的种类
种类名称 执行情况 Serial Dispatch Queue 等待现在执行中处理结束 Concurrent Dispatch Queue 不等待现在执行处理结束

准备以下源代码,在dispatch_async中追加多个处理。

// Demo code 1
dispatch_async(queue, blk0);
dispatch_async(queue, blk1);
dispatch_async(queue, blk2);
dispatch_async(queue, blk3);
dispatch_async(queue, blk4);
dispatch_async(queue, blk5);
dispatch_async(queue, blk6);
dispatch_async(queue, blk7);

当变量queueSerial Dispatch Queue 时,因为要等待现在执行中的处理结束,所以首先执行任务 blk0 ,待 blk0 执行结束后,接着执行 blk1, blk1 结束后再开始执行 blk2,如此重复。同时执行的任务只能为1个。即执行源代码片段Demo code 1后,按照以下顺序进行处理。

blk0
blk1
blk2
blk3
blk4
blk5
blk6
blk7

但当变量 queueConcurrent Dispatch Queue 时,因为不用等待现在执行中的处理结束,所以执行顺序将变成:
首先执行 blk0, 不管 blk0 的执行是否结束,都开始执行后面的 blk1, 不管 blk1 的执行是否结束,都开始执行后面的 blk2, 如此重复循环。

这样虽然不用等待处理结束,可以并行执行多个处理,但并行执行的处理数量取决于当前系统的状态。 即 iOS 和 OS X 基于 Dispatch Queue 中的处理数、CPU 核数及 CPU 负荷等当前系统的状态来决定 Concurrent Dispatch Queue 中并行执行的处理数。

所谓“并行执行”,就是使用多个线程同时执行多个处理。

总结来说,Serial Dispatch Queue 使用一个线程;而 Concurrent Dispatch Queue 使用的是多个线程。

iOS 和 OS X的核心– XNU 内核决定应当使用的线程数,并只生成所需的线程执行处理。另外,当处理结束,应当执行的处理数减少时,XNU内核会结束不再需要的线程。 XNU 内核仅使用 Concurrent Dispatch Queue 便可完美地管理并行执行多个处理的线程。

假设准备4个 Concurrent Dispatch Queue 使用的线程。首先:blk0 在线程0中开始执行,接着blk1在线程1中、blk2在线程2中、blk3在线程3中开始执行。线程0中blk0执行结束后,开始执行blk4,由于线程1中blk1的执行没有结束,因此线程数中blk2执行结束后开始执行blk5,就这样循环往复。

        **Concurrent Dispatch Queue** 执行示例
thread0 thread1 thread2 thread3 blk0 blk4 blk7 blk1 blk5 blk2 blk3

像这样在Concurrent Dispatch Queue中执行处理时,执行顺序会根据处理内容和系统状态发生改变。 它不同于执行顺序固定的 Serial Dispatch Queue。在不能改变执行的处理顺序或不想并行执行多个处理时使用 Serial Dispatch Queue

那么如何得到这两种Queue呢?方法有两种。

2. dispatch_queue_create

第一种方式是使用GCD的API生成 Dispatch Queue.
通过 dispatch_queue_create 函数可生成 Dispatch Queue。 以下源代码生成了 Serial Dispatch Queue

dispatch_queuet mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);

根据苹果的API函数声明:

/*!
 * @function dispatch_queue_create
 *
 * @abstract
 * Creates a new dispatch queue to which blocks may be submitted.
 *
 * @discussion
 * Dispatch queues created with the DISPATCH_QUEUE_SERIAL or a NULL attribute
 * invoke blocks serially in FIFO order.
 *
 * Dispatch queues created with the DISPATCH_QUEUE_CONCURRENT attribute may
 * invoke blocks concurrently (similarly to the global concurrent queues, but
 * potentially with more overhead), and support barrier blocks submitted with
 * the dispatch barrier API, which e.g. enables the implementation of efficient
 * reader-writer schemes.
 *
 * When a dispatch queue is no longer needed, it should be released with
 * dispatch_release(). Note that any pending blocks submitted to a queue will
 * hold a reference to that queue. Therefore a queue will not be deallocated
 * until all pending blocks have finished.
 *
 * Passing the result of the dispatch_queue_attr_make_with_qos_class() function
 * to the attr parameter of this function allows a quality of service class and
 * relative priority to be specified for the newly created queue.
 * The quality of service class so specified takes precedence over the quality
 * of service class of the newly created dispatch queue's target queue (if any)
 * as long that does not result in a lower QOS class and relative priority.
 *
 * When no quality of service class is specified, the target queue of a newly
 * created dispatch queue is the default priority global concurrent queue.
 *
 * @param label
 * A string label to attach to the queue.
 * This parameter is optional and may be NULL.
 *
 * @param attr
 * DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_CONCURRENT, or the result of a call to
 * the function dispatch_queue_attr_make_with_qos_class().
 *
 * @result
 * The newly created dispatch queue.
 */
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT
DISPATCH_NOTHROW
dispatch_queue_t
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);

dispatch_queue_create 函数的第一个参数是表示队列名;第二个参数表示队列的种类,声明为NULL表示串行队列,与参数DISPATCH_QUEUE_SERIAL 同样的效果表示串行队列。

关于 Serial Dispatch Queue生成个数的注意事项如下:Serial Dispatch Queue 同时只能执行1个追加处理。虽然 Serial Dispatch QueueConcurrent Dispatch Queue 受到系统资源的限制,但用 dispatch_queue_create 函数可生成任意多个 Dispatch Queue

当生成多个 Serial Dispatch Queue 时,在每个 Serial Dispatch Queue 中,同时只能执行一个追加处理,但各个 Serial Dispatch Queue 将并行执行,达到同时执行多个任务处理的效果。一旦生成 Serial Dispatch Queue 并追加处理,系统对于一个 Serial Dispatch Queue 就只生成并使用一个线程。 如果生成2000个 Serial Dispatch Queue, 那么就生成2000 个线程。

只在避免多线程编程问题之一 ——— 多个线程更新相同资源导致数据竞争时使用 Serial Dispatch Queue

Serial Dispatch Queue 的生成个数应当仅限所必需的数量。 例如更新数据库时1 个表生成1 个 Serial Dispatch Queue,更新文件时1个文件或是可以分割的1个文件块生成1个 Serial Dispatch Queue. 虽然“Serial Dispatch QueueConcurrent Dispatch Queue 能生成更多的线程”,但绝不能激动之下大量生成 Serial Dispatch Queue

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

继续讲 dispatch_queue_create 函数。该函数的第一个参数指定 Serial Dispatch Queue 的名称。像此源代码这样, Dispatch Queue 的名称推荐使用应用程序 ID 这种逆序全程域名(FQDN,fully qualified domain name)。该名称在 XCode 和 Instruments 的调试器中作为 Dispatch Queue 名称来表示。另外,该名称也会出现在应用程序崩溃时所生成的 crashLog 中。 我们命名时应遵循这样的原则:对编程人员和用户来说都要易懂。如果嫌命名麻烦设为 NULL 也可以,但在调试中一定会后悔没有为 Dispatch Queue 署名。

生成 Serial Dispatch Queue 时,像该源代码这样,将第二个参数指定为NULL。生成Concurrent Dispatch Queue时,像下面源代码一样,指定为 DISPATCH_QUEUE_CONCURRENT

dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.example.gcd.MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(myConcurrentDispatchQueue,
^{NSLog(@"block on myConcurrentDispatchQueue");});

并在生成的 Concurrent Dispatch Queue 中执行指定的Block。

通过 dispatch_queue_create 函数生成的 Dispatch Queue 在使用结束后通过 dispatch_release 函数释放。

dispatch_release(mySerialDispatchQueue);

相应地,也存在dispatch_retain函数。

dispatch_retain(myConcurrentDispatchQueue);

即Dispatch Queue也像 Objective-C的饮用技术式内存管理一样,需要通过 dispatch_retain 函数和 dispatch_release 函数的引用计数来管理内存(ARC 模式下不用)。在前面的源代码中,需要释放通过 dispatch_queue_create 函数生成并赋值给变量 myConcurrentDispatchQueue 中的 Concurrent Dispatch Queue
再看一个例子:

dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.demo.gcd.myConcurrentDispatchQueue",DISPATCH_QUEUE_CONCURRENT);

dispatch_async(myConcurrentDispatchQueue,
^{NSLog(@"block on myConcurrentDispatchQueue");});

dispatch_release(myConcurrentDispatchQueue);

虽然 Concurrent Dispatch Queue 是使用多线程进行的追加处理,但像该例这样,在 dispatch_async 函数中追加 Block 到 Concurrent Dispatch Queue,并立即通过 dispatch_release 函数进行释放是否可以呢?

该源代码完全没有问题。在 dispatch_async 函数中追加 Block 到 Dispatch Queue ,换言之:该 Block 通过 dispatch_retain 函数持有 Dispatch Queue。无论 Dispatch Queue 是 Serial Dispatch Queue 还是 Concurrent Dispatch Queue 都一样。一旦Block 执行结束,就通过 dispatch_release 函数释放该 Block 持有的 Dispatch Queue。

也就是说,在 dispatch_async 函数中追加 Block 到 Dispatch Queue 后,即使立即释放 Dispatch Queue,该 Dispatch Queue 由于被 Block 所持有也不会被废弃,因而 Block 能够执行。 Block 执行结束后会释放 Dispatch Queue,这时谁都不持有 Dispatch Queue,因此它会被释放。

另外,能够使用 dispatch_retain 函数和 dispatch_release 函数的地方不仅是在 Dispatch Queue 中。在之后介绍的几个 GCD 的 API中,名称中含有“create”的 API 在不需要其生成的对象时,有必要通过 dispatch_release 函数进行释放。在通过函数或方法获取 Dispatch Queue 以及其它名称中含有 create 的 API 生成的对象时,有必要通过 dispatch_retain 函数持有,并在不需要时通过 dispatch_release 函数释放。

3. Main Dispatch Queue / Global Dispatch Queue

第二种方法是获取系统标准提供的Dispatch Queue。
实际上不用特意生成 Dispatch Queue 系统也会给我们提供几个。那就是 Main Dispatch Queue 和 Global Dispatch Queue。

3.1 Main Dispatch Queue
主线程中执行的 Dispatch Queue,因为主线程只有一个,所以 Main Dispatch Queue 自然就是 Serial Dispatch Queue。
追加到 Main Dispatch Queue 中的任务在主线程的 RunLoop 中执行。由于在主线程中执行,因此将用户界面更新等一些必须在主线程中执行的处理追加到 Main Dispatch Queue 中使用。这正好和 NSObject 类的 performSelectorOnMainThread 实例方法相同。

3.2 Global Dispatch Queue
所有程序都可以使用,没有必要通过 dispatch_queue_create 函数逐个生成 Concurrent Dispatch Queue 。只要获取 Global Dispatch Queue 即可。
Global Dispatch Queue 具有4个优先级,分别是:
1)高优先级 (High Priority)
2) 默认优先级(Default Priority)
3) 低优先级(Low Priority)
4) 后台优先级 (Background Priority).

通过 XNU 内核管理的用于 Global Dispatch Queue 的线程,将各自使用的 Global Dispatch Queue 的执行优先级作为线程的优先级来使用。在向 Global Dispatch Queue 追加处理时,应选择与处理内容执行优先级一致的 Global Dispatch Queue。

但是通过XNU内核用于 Global Dispatch Queue 的线程并不能保证实时性,因此执行优先级只是大致的判断。例如在处理内容的执行可有可无时,使用后台优先级的 Global Dispatch Queue 等,只能进行这种程度的划分。系统提供的 Dispatch Queue 总结如下表:

                 Dispatch Queue 的种类
名称 种类 说明 Main Dispatch Queue Serial Dispatch Queue 主线程执行 Global Dispatch Queue(High Priority) Concurrent Dispatch Queue 执行优先级:高 Global Dispatch Queue(Default Priority) Concurrent Dispatch Queue 执行优先级:默认 Global Dispatch Queue(Low Priority) Concurrent Dispatch Queue 执行优先级:低 Global Dispatch Queue(Background Priority) Concurrent Dispatch Queue 执行优先级:后台

各种 Dispatch Queue 的获取方法如下:

//Main Dispatch Queue的获取方法
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();

//Global Dispatch Queue(高优先级)的获取方法
dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY,0);

//Global Dispatch Queue(默认优先级)的获取方法
dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);

//Global Dispatch Queue(低优先级)的获取方法
dispatch_queue_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,0);

//Global Dispatch Queue(后台优先级)的获取方法
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);

对于Main Dispatch Queue和 Global Dispatch Queue执行 dispatch_retain 和 dispatch_release 函数不会引起任何变化,也不会有任何问题。这也是获取并使用 Global Dispatch Queue 比生成、使用、释放 Concurrent Dispatch Queue轻松的原因。

当然,源代码上进行类似通过 dispatch_queue_create 函数生成 Dispatch Queue 的处理要更轻松时,可参照引用计数内存管理的思维方式,直接在 Main Dispatch Queue 和 Global Dispatch Queue中执行 dispatch_retain 和 dispatch_release 函数。

使用 Main Dispatch Queue 和 Global Dispatch Queue 的源代码如下:

//在默认优先级的Global Dispatch Queue中执行 Block
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{
    //可并行执行的处理

    //在 Main Dispatch Queue 中执行 Block
    dispatch_async(dispatch_get_main_queue(),^{
        //只能在主线程中进行的处理
        //比如:UI操作
    });
});

4. dispatch_set_target_queue

dispatch_queue_create 函数生成的 Dispatch Queue 不管是 Serial Dispatch Queue 还是 Concurrent Dispatch Queue, 都使用与默认优先级 Global Dispatch Queue 相同执行优先级的线程。而变更生成的 Dispatch Queue 的执行优先级要使用 dispatch_set_target_queue 函数。在后台执行动作处理的 Serial Dispatch Queue 的生成方法如下:

dispatch_queue_t mySerialDispatachQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue",NULL);
dispatch_queue_t globalDispatachQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);
dispatch_set_target_queue(mySerialDispatchQueue,globalDispatchQueueBackground);

指定要变更执行优先级的 Dispatch Queue 为 dispatch_set_target_queue 函数的第一个参数,第二个参数指定为与要使用的执行优先级相同优先级的 Global Dispatch Queue。前者为待变更的 Dispatch Queue, 后者为指定优先级的目标 Dispatch Queue。

第一个参数如果指定系统提供的 Main Dispatch Queue 和 Global Dispatch Queue 则不知道会出现什么状况,因此这些均不可指定。

将 Dispatch Queue 指定为 dispatch_set_target_queue 函数的参数,不仅可以变更 Dispatch Queue的执行优先级,还可以作为 Dispatch Queue 的执行阶层。如果在多个 Serial Dispatch Queue 中用 dispatch_set_target_queue 函数指定目标为某一个 Serial Dispatch Queue,那么原先本应并行执行的多个 Serial Dispatch Queue,在目标 Serial Dispatch Queue 上只能同时执行一个处理。

在必须将不可并行执行的处理追加到多个 Serial Dispatch Queue 中时,如果使用 dispatch_set_target_queue 函数将目标指定为某一个 Serial Dispatch Queue,即可防止处理并行执行。

5. dispatch_after

想在指定时间后执行处理,可使用 dispatch_after 函数来实现。
在3秒后将指定的 Block 追加到 Main Dispatch Queue 中的源代码如下:

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

需要注意(⚠️)的是:dispatch_after 函数并不是在指定时间后执行处理,而只是在指定时间后追加处理到 Dispatch Queue。上述源代码与在3秒后用 dispatch_async函数追加 Block 到 Main Dispatch Queue 的相同。

因为 Main Dispatch Queue 在主线程的 RunLoop 中执行, 所以在比如每隔 1/60 秒执行的 RunLoop 中,Block 最快在 3 秒后执行,最慢在 (3+1/60)秒后执行,并且在 Main Dispatch Queue 中有大量处理任务或主线程的处理本身有延迟时,这个时间会更长。

虽然在严格的时间要求下使用会出现问题,但在大致想延迟执行处理时,该函数非常有效。

另外,第二个参数指定要追加处理的 Dispatch Queue, 第三个参数指定记述要处理的Block。

第一个参数是指定时间用的 dispatch_time_t 类型的值。该值可以使用dispatch_time 或 dispatch_walltime 函数生成。
1)dispatch_time 生成函数
计算相对时间

dispatch_time_t
dispatch_time(dispatch_time_t when, int64_t delta);

中第一个参数when起始的时间开始,到第二个参数delta指定的毫微秒单位时间后的时间。when经常使用的值是之前源代码中出现的

DISPATCH_TIME_NOW

这表示现在的时间,即以下源代码可得到表示从现在开始1秒后的dispatch_time_t 类型的值。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW,1ull * NSEC_PER_SEC);

数值和 NSEC_PER_SEC的乘积得到单位为毫微秒的数值。“ull”是C语言的数值字面量,是显示表明类型时使用的字符串(表示“unsigned long long”)。如果使用 NSEC_PER_MSEC则可以以毫秒为单位计算。以下代码获取表示从现在开始150毫秒后时间的值。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW,150ull * NSEC_PER_MSEC);

2) dispatch_walltime 函数由POSIX中使用的 struct timespec 类型的时间得到 dispatch_time_t 类型的值。 用于计算绝对时间。例如在 dispatch_after 函数中想指定 2011 年 11 月 11 日 11 时 11 分 11秒 这一绝对时间的情况,这可作为粗略的闹钟功能使用。

struct timespec 类型的时间可以很轻松地通过 NSDate 类对象生成。下面是示例代码:

dispatch_time_t getDispatchTimeByDate(NSDate *date) {
    NSTimeInterval interval;
    double second, subsecond;
    struct timespec time;
    dispatch_time_t milestone;

    interval = [date timeIntervalSince1970];
    subsecond = modf(interval, &second);
    time.tv_sec = second;
    time.tv_nsec = subsecond * NSEC_PER_SEC;
    milestone = dispatch_walltime(&time, 0);

    retuen milestone;
}

其中modf函数是获取double数值的分数(小数)部分的值,同时将整数部分存储到第二个参数 second中,参考这个英文网站这个中文网站的解释:

推荐:[置顶] iOS_多线程_GCD

1、GCD串行队列+异步dispatch 说明: 非常非常非常有用的操作 异步,表示会在主线程之外开新的线程,但由于 队列是串行的,故只开 一个(只开一个)新的线程 当dispatc

function: modf (param , &intpart)
Break into fractional and integral parts
Breaks x into an integral and a fractional part.

The integer part is stored in the object pointed by intpart, and the
fractional part is returned by the function.

Both parts have the same sign as x.

6. Dispatch Group

在此种情况下可以使用Dispatch Group。比如:追加3个Block到 Global Dispatch Queue, 这些 Block 如果全部执行完毕,就会执行 Main 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_release(group);

经过实际测试:

dispatch_release(group);

代码必须注释掉,因为在ARC环境下该代码会报错:
“ARC forbids explicit message send of ‘release’”.
该源代码的执行结果如下:

blk0
blk2
blk1
done

因为向 Global Dispatch Queue 即 Concurrent Dispatch Queue 追加处理, 多个线程并行执行,所以追加处理的执行顺序不定。执行时的顺序会发生变化,但是最后输出 done。

无论向什么样的 Dispatch Queue 中追加处理,使用 Dispatch Group 都可监视这些处理执行的结束。一旦检测到所有处理执行结束,就可将结束的处理追加到 Dispatch Queue 中。这就是使用 Dispatch Group 的原因。

首先 dispatch_group_create 函数生成 dispatch_group_t 类型的Dispatch Group。如 dispatch_group_create 函数名中所含的 create所示,该 Dispatch Group 与 Dispatch Queue 相同,在使用结束后需要通过 dispatch_release 函数释放(在ARC模式下不用)。

另外,与追加Block到 Dispatch Queue 时同样,Block通过 dispatch_retain 函数持有 Dispatch Group,从而使得该 Block 属于 Dispatch Group。这样如果 Block 执行结束,该 Block 就通过dispatch_release 函数释放持有的 Dispatch Group。 一旦 Dispatch Group 使用结束,不用考虑属于该 Dispatch Group 的 Block,立即通过 dispatch_release 函数释放即可。

在追加到 Dispatch Group 中的处理全部执行结束时,该源代码中使用的 dispatch_group_notify 函数会将执行的 block 追加到 queue中,将第一个参数指定为要监视的 group。在追加到该 Dispatch Group 的全部处理执行结束时,将第三个参数的 block 追加到第二个参数的 queue(Dispatch Queue)中。在 dispatch_group_notify函数中不管指定什么样的 Dispatch Queue,属于 Dispatch Group的全部处理在追加指定的 Block 时都已执行结束。
函数原型为:

dispatch_group_notify(dispatch_group_t group,
    dispatch_queue_t queue,
    dispatch_block_t block);

另外,在 Dispatch Group 中也可以使用 dispatch_group_wait 函数等待全部处理执行结束。

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_wait(group, DISPATCH_TIME_FOREVER);

dispatch_group_wait 的函数原型为:

/*!
 * @function dispatch_group_wait
 *
 * @abstract
 * Wait synchronously until all the blocks associated with a group have
 * completed or until the specified timeout has elapsed.
 *
 * @discussion
 * This function waits for the completion of the blocks associated with the
 * given dispatch group, and returns after all blocks have completed or when
 * the specified timeout has elapsed. When a timeout occurs, the group is
 * restored to its original state.
 *
 * This function will return immediately if there are no blocks associated
 * with the dispatch group (i.e. the group is empty).
 *
 * The result of calling this function from multiple threads simultaneously
 * with the same dispatch group is undefined.
 *
 * After the successful return of this function, the dispatch group is empty.
 * It may either be released with dispatch_release() or re-used for additional
 * blocks. See dispatch_group_async() for more information.
 *
 * @param group
 * The dispatch group to wait on.
 * The result of passing NULL in this parameter is undefined.
 *
 * @param timeout
 * When to timeout (see dispatch_time). As a convenience, there are the
 * DISPATCH_TIME_NOW and DISPATCH_TIME_FOREVER constants.
 *
 * @result
 * Returns zero on success (all blocks associated with the group completed
 * within the specified timeout) or non-zero on error (i.e. timed out).
 */
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

timeout 指定等待的时间,DISPATCH_TIME_FOREVER 意味着永久等待。只要属于 Dispatch Group 的处理尚未执行结束,就会一直等待,中途不能取消。此时返回值恒为0,处理必定全部执行结束。

若函数 dispatch_group_wait 返回值不为0,表示经过指定的时间,Dispatch Group 中的任务处理还未结束(还在执行中)。
例如,可以根据返回值来进行相应的处理:
指定等待时间为1秒。

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);

    long result = dispatch_group_wait(group, time);
    if (0 == result) {
        //group 中的任务处理结束
    } else {
        //group 中的某一任务还在执行中
    }

“等待”意味着一旦调用 dispatch_group_wait 函数,该函数就处于调用的状态而不返回。即执行 dispatch_group_wait 函数的线程(当前线程)停止。在经过 dispatch_group_wait 函数中指定的时间或属于指定 Dispatch Group 的处理全部执行结束之前,执行该函数的线程停止。

指定 DISPATCH_TIME_NOW,则不用任何等待即可判定属于Dispatch Group 的处理是否执行结束。

long result = dispatch_group_wait(group, DISPATCH_TIME_NOW);

在主线程的 RunLoop 的每次循环中, 可检查执行是否结束,从而不耗费多余的等待时间,虽然也可以这样做,但推荐使用dispatch_group_notify 函数追加结束处理到 Main Dispatch Queue中,以简化源代码。

7. dispatch_barrier_async

访问数据库或文件时,使用 Serial Dispatch Queue 可避免数据竞争的问题。
写入处理确实不可与其他的写入处理以及包含读取处理的其他某些处理并行执行,但是如果并行执行读取处理,是不会发生问题的。

也就是说,为了高效率地进行访问,读取处理追加到 Concurrent Dispatch Queue 中,写入处理在任一个读取处理没有执行的状态下,追加到 Serial Dispatch Queue 中即可(写入处理结束之前,读取处理不可执行)。

虽然使用 Dispatch Group 和 dispatch_set_target_queue 函数也可实现,但是源代码也会很复杂。

GCD为我们提供了更为聪明的解决办法– dispatch_barrier_async 函数。该函数同 dispatch_queue_create 函数生成的 Concurrent Dispatch Queue 一起使用。

步骤为:
首先,创建 Concurrent Dispatch Queue,在 dispatch_async 中追加读取处理。
在需要追加写入操作的位置,加入 dispatch_barrier_async 函数;
等待追加到 Concurrent Dispatch Queue 上的并行执行的处理全部执行结束之后,再将指定的处理追加到 Concurrent Dispatch Queue 中。
然后在 dispatch_barrier_async 函数追加的处理执行完毕后,Concurrent Dispatch Queue 才恢复对其中的任务进行并行处理。
代码如下:

    dispatch_async(queue, blk0_for_reading);
    dispatch_async(queue, blk1_for_reading);
    dispatch_async(queue, blk2_for_reading);
    dispatch_async(queue, blk3_for_reading);
    dispatch_barrier_async(queue, blk_for_writing);
    dispatch_async(queue, blk4_for_reading);
    dispatch_async(queue, blk5_for_reading);
    dispatch_async(queue, blk6_for_reading);
    dispatch_async(queue, blk7_for_reading);

使用 Concurrent Dispatch Queue 和 dispatch_barrier_async 函数可实现高效率的数据库访问和文件访问。

8. dispatch_sync

sync意味着“同步”(synchronous),也就是将指定的 Block “同步”追加到指定的 Dispatch Queue 中。在追加 Block 结束之前,dispatch_sync 函数会一直等待。

与之相对的是 dispatch_async, 其中的“async”意味着“非同步”(asynchronous),就是将指定的 Block “非同步” 地追加到指定的 Dispatch Queue 中。dispatch_async 函数不做任何等待。
如 dispatch_group_wait 函数说明所示,“等待”意味着当前线程停止。

我们先假设这样一种情况:执行 Main Dispatch Queue时,使用另外的线程 Global Dispatch Queue 进行处理,处理结束后立即使用所得到的结果。在这种情况下就要使用 dispatch_sync 函数。

dispatch_queue queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);

dispatch_sync(queue,^{
    //同步处理
});

调用 dispatch_sync 函数,在处理执行结束之前,该函数不会返回。 dispatch_sync 函数可以简化源代码,也可以说是简易版的 dispatch_group_wait 函数。

正因为 dispatch_sync 函数使用简单,所以也容易引起死锁。
例如,在主线程中使用执行下述源代码会有死锁问题。

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
    NSLog(@"Hello?");
});

该源代码在 Main Dispatch Queue 即主线程中执行Block,并等待其执行结束。而主线程中正在执行这些源代码,所以无法执行追加到 Main Dispatch Queue 的 Block。下面的例子也是:

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue,^{
    dispatch_sync(queue,^{
        NSLog(@"Hello?");
    });
});

Main Dispatch Queue 中执行的 Block 等待 Main Dispatch Queue 中要执行的 Block 执行结束。 这样的死锁就像在画像上画画一样。
改成异步执行即可:

dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"Hello?");
    });

Serial Dispatch Queue 也会引起相同的问题。

dispatch_queue_t queue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
dispatch_async(queue, ^ {
    dispatch_sync(queue,^{
        NSLog(@"Hello?");
    });
});

另外,由 dispatch_barrier_async 函数中含有 async 可推测出,相应的也有 dispatch_barrier_sync 函数。 dispatch_barrier_async 函数的作用是在等待追加的处理全部执行结束后,再追加处理到 Dispatch Queue 中,此外,它还与 dispatch_sync 函数相同,会等待追加处理的执行结束。只是执行的方式不同:dispatch_sync 同步执行,dispatch_async 异步执行。

/*!
* @function dispatch_barrier_sync *
*
* @abstract * Submits a barrier block for synchronous execution on a dispatch queue. */
/*!
* @function dispatch_barrier_async
*
* @abstract
* Submits a barrier block for asynchronous execution on a dispatch queue.*/

参考这篇博客的总结:
两者的区别是:
dispatch_barrier_sync 需要在自己的任务处理结束之后,再将dispatch_barrier_sync 之后的任务插入队列中。

9. dispatch_apply

dispatch_apply 函数是 dispatch_sync 函数和 Dispatch Group 的关联 API。该函数按指定的次数将指定的 Block 追加到指定的 Dispatch Queue 中,并等待全部处理执行结束。

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

因为在 Global Dispatch Queue 中执行处理, 所以各个处理的执行时间不定。但是输出结果中最后的 done 必定在最后的位置上。这是因为 dispatch_apply 函数会等待全部处理执行结束。

dispatch_apply函数的第1个参数是重复次数,第2个参数是加入的 Dispatch Queue, 第3个参数为带有参数的 Block。 这是为了按第1个参数重复追加 Block 并区分各个 Block 而使用。例如要对 NSArray 类对象的所有元素执行处理时,不必一个一个进行 for 循环遍历。

看看对 NSArray 类对象进行 for 循环遍历的代码:

NSArray *array = @[@(1),@(2),@(3),@(4),@(5)];
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply([array count], queue, ^(size_t index) {
        NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
    });
});

这样可简单地在 Global Dispatch Queue 中对所有元素执行 Block。
假设不要求 array 中元素处理的顺序,可以进行上面的操作。
另外,由于 dispatch_apply 函数也与 dispatch_sync 函数相同,会等待处理执行结束,因此推荐在 dispatch_async 函数中非同步地执行 dispatch_apply 函数。

dispatch_queue_t queue = dispatch_get_main_queue();
    //在 Global Dispatch Queue 中非同步地执行
    dispatch_async(queue, ^{
        // Global Dispatch Queue 等待 dispatch_apply 函数中全部处理执行结束
        dispatch_apply([array count], queue, ^(size_t index) {
            //并行处理包含在 NSArray 对象中的全部对象
            NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
        });
    });

    //dispatch_apply 函数中的处理全部执行结束
    //在 Main Dispatch Queue 中非同步执行
    dispatch_async(dispatch_get_main_queue(), ^{
        //在 Main Dispatch Queue 中执行处理 比如界面更新等
        NSLog(@"Done");
    });

10. dispatch_suspend / dispatch_resume

当追加大量处理到 Dispatch Queue 时,在追加处理的过程中,有时希望不执行已追加的处理。例如当 Block 截获演算结果时,一些处理会对这个演算结果造成影响。

在这种情况下,只要挂起 Dispatch Queue 即可。当可以执行时再回复。
dispatch_supend 函数挂起指定的queue;

dispatch_supend(queue);

dispatch_resume 函数恢复指定的queue;

dispatch_resume(queue);

这些函数对已经执行的处理没有影响。 挂起后, 追加到 Dispatch Queue 中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。

11. Dispatch Semaphore

持有计数的信号,执行处理时进行更细粒度的排他控制。该计数是多线程编程中的计数类型信号。计数的含义为:0为等待;1或大于1时,减去1而不等待。
使用方法为:
1. 生成

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

参数表示计数的初始值。本例将计数值初始化为“1”。
2. 判断计数值,执行操作

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    long result = dispatch_semaphore_wait(semaphore, time);

    if (0 == result) {
        // 由于 Dispatch Semaphore 的计数值达到等于或大于1
        // 或者在待机中的指定时间内
        // Dispatch Semaphore 的计数值达到大于或等于1
        // 所以 Dispatch Semaphore 的计数值减1
        // 可执行排他控制的处理
    } else {
        // 由于 Dispatch Semaphore 的计数值为 0
        // 因此在达到指定时间为止待机
    }

dispatch_semaphore_wait 函数返回 0 时,可安全地执行需要进行排他控制的处理。该处理结束时通过 dispatch_semaphore_signal 函数将 Dispatch Semaphore 的计数值加1.

等待 Dispatch Semaphore 的计数值达到大于或等于1 。当计数值大于或等于1,或者在待机中计数值大于等于1时,对该计数进行减1并返回。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //生成 Dispatch Semaphore. 并将初始值设定为1,
    //保证可以访问NSMutableArray 类对象的线程同时只有1个
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    NSMutableArray *array = [[NSMutableArray alloc] init];

    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            //等待计数值大于等于1
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);


            //由于 Dispatch Semaphore 的计数值大于或等于1
            //所以 Dispatch Semaphore 的计数值减1
            //即执行到此的Dispatch Semaphore 的计数值恒为 “0”
            //由于可同时访问的类对象线程只有1个,因此可安全更新
            [array addObject:[NSNumber numberWithInt:i]];

            //排他控制处理结束
            //所以通过 dispatch_semaphore_signal 函数
            //将 Dispatch Semaphore 的计数值加1
            //如果有通过 dispatch_semaphore_wait等待Dispatch Semaphore 的计数值
            //加1 的线程,就由最先等待的线程执行
            dispatch_semaphore_signal(semaphore);
        });
    }

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

12. dispatch_once

常在单例模式下保证执行一次处理。

static dispatch_once_t pred;
dispatch_once(&pred, ^{
    //block
});

在多线程模式下执行,也可保证百分之百安全。

13. Dispatch I/O

在读取大文件时,可将文件分成合适的大小并使用 Global Dispatch Queue 并列读取,会比一般的读取速度快不少。能实现这一功能的是 Dispatch I/O 和 Dispatch Data。

通过 Dispatch I/O 读写文件时,使用 Global Dispatch Queue 将1个文件按某个大小 read/write。

    dispatch_async(queue, ^{
        //读取 0 - 8191 字节
    });
    dispatch_async(queue, ^{
        //读取 8192 - 16383 字节
    });
    dispatch_async(queue, ^{
        //读取 16384 - 24575 字节
    });

像上面这样,将文件分割为一块一块地进行读取处理。分割读取的数据通过使用 Dispatch Data 可更为简单地进行结合和分割。
如果像提高文件读取速度,可以尝试使用 Dispatch I/O.

三 总结

1. 管理方式

GCD 的管理是队列式,分为串行队列并行队列两种。前者使用 FIFO 方式管理,后者由系统根据资源和系统负荷等情况,自行控制进行队列任务的并发线程数量。

2. 队列

系统内置了主线程队列 (Main Dispatch Queue) 和全局并发队列(Global Dispatch Queue), 前者是一个串行队列,可通过dispatch_get_main_queue()获取;后者具有4个优先级的并发队列,可通过 dispatch_get_global_queue获取。也可以使用dispatch_queue_create函数自行创建队列。

更改某个队列的优先级可以使用 dispatch_set_target_queue 函数来指定。

3. 任务组

除此之外,GCD 提供了 Dispatch Group 来进行多个任务的组管理,可以设置某组任务完成之后自动通知( dispatch_group_notify )进行下一步处理;或者等待某一操作完成 (dispatch_group_wait)。

4. 执行

4.1 同步执行
同步执行会等待当前任务处理完毕,再执行。
相应的是 dispatch_sync();
4.2 异步执行
异步执行会自动提交当前任务,不等待,继续原 queue 队列中的任务。
相应的是 dispatch_async();
4.3 阻塞执行
阻塞执行会等待队列中的所有任务执行,然后再执行当前任务,最后执行队列中的剩余任务。
相应的函数是 dispatch_barrier_sync()dispatch_barrier_async();
4.4 一次执行
确保多线程时,该代码只被执行一次
常用于单例模式下,单例对象的生成。
相应的函数为 dispatch_once;
4.5 多次执行
重复多次执行某个任务时,可以使用 dispatch_apply 函数指定多个任务,每个任务在待索引的 Block 中指定;
4.6 延迟执行
指定当前任务的执行时间,可以指定相对当前的一个相对时间或某一具体的绝对时间
相应的函数为 dispatch_after;

5. 细粒度控制

使用带计数值的 dispatch_semaphore, 指定计数值表示对排他控制操作的操作个数,在进行排他控制时使用 dispatch_semaphore_wait 函数等待;在排他控制完毕时使用 dispatch_semaphore_signal 函数通知等待的线程

6. Dispatch I/O

使用 Dispatch I/O ,在 Global Dispatch Queue中,结合 Dispatch Data 对大文件进行并行的读写操作,提高读写效率。

推荐:iOS多线程编程Part 3/3 - GCD

原文:http://www.hrchen.com/2013/07/multi-threading-programming-of-ios-part-3/ 前两部分介绍了NSThread、NSRunLoop和NSOperation,本文聊聊2011年WWDC时推

GCD 是 iOS 编程中实现多线程的常用 API,使用方便,无须进行线程的管理,由系统代劳。同时GCD基于C实现,性能强。 在名著《Pro multithreading and memory management for iOS and OS X》(中

相关阅读排行


相关内容推荐

最新文章

×

×

请激活账号

为了能正常使用评论、编辑功能及以后陆续为用户提供的其他产品,请激活账号。

您的注册邮箱: 修改

重新发送激活邮件 进入我的邮箱

如果您没有收到激活邮件,请注意检查垃圾箱。