OC高级编程-IOS与OS X多线程和内存管理--3.GrandCentralDispatch

1.Grand Central Dispatch概要

1.1 什么是GCD

Grand Central Dispatch ( GCD)是异步执行任务的技术之一。 一般将应用程序中记述的线程管理用的代码在系统级中实现。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中, GCD 就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前的线程更有效率。

1.2 多线程编程

线程:1个CPU 执行的CPU 命令列为一条无分叉路径。
多线程:这种无分叉路径不只1 条,存在有多条。
上下文切换:CPU切换执行路径时,执行中路径的状态(例如CPU 的寄存器等信息)保存到各自路径专用的内存块中,以及从切换目标路径专用的内存块中复原CPU 寄存器等信息,继续执行切换路径的CPU 命令列。
多线程编程:使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,因此看上去就好像1个CPU 核能够并列地执行多个线程一样。
长时间的处理不在主线程中执行而在其他线程中执行:长时间的处理会妨碍主线程中被称为RunLoop 的主循环的执行,从而导致不能更新用户界面、应用程序的画面长时间停滞等问题。
过多使用多线程,会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能。

2.GCD的API

2.1 Dispatch Queue

开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue 中。

1
2
3
4
//向queue中追加要执行的任务
dispatch_async(queue, ^{
//要执行的任务
});

Dispatch Queue是执行处理的等待队列,按照FIFO来处理。

表1 Dispatch Queue的种类
Dispatch Queue的种类 说明
Serial Dispatch Queue 等待现在执行中处理结束
Concurrent Dispatch Queue 不等待现在执行中处理结束
![图3.1 SerialDispatchQueue、ConcurrentDispatchQueue 和线程的关系](https://github.com/MrConfused/img/blob/master/IOS/OC/OC%E9%AB%98%E7%BA%A7%E7%BC%96%E7%A8%8B-IOS%E4%B8%8EOS_X%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%92%8C%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/%E5%9B%BE3.1%20SerialDispatchQueue%E3%80%81ConcurrentDispatchQueue%20%E5%92%8C%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%85%B3%E7%B3%BB.png?raw=true)
图3.1 SerialDispatchQueue、ConcurrentDispatchQueue 和线程的关系

Serial Dispatch Queue可用于解决多线程的数据竞争问题(多个线程竞争同一个资源);但是若生成了多个Serial Dispatch Queue,那么就能同时执行多个线程,因此绝不能大量生成Serial Dispatch Queue。

Concurrent Dispatch Queue适用于没有数据竞争问题的情况,而且不管有多少个Concurrent Dispatch Queue,XNU内核只使用有效管理的线程,不会同时执行过多线程

2.2 dispatch_queue_create

用于生成Dispatch Queue

1
2
dispatch_queue_t mySerialDispatchQueue = 
dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
  • 第一个参数:指定Dispatch Queue的名称,推荐使用应用程序ID这种逆序全程域名( FQDN , fully qualified domain name)。
  • 第二个参数:指定Dispatch Queue的类型。NULL生成Serial Dispatch Queue,DISPATCH_QUEUE_CONCURRENT生成Concurrent Dispatch Queue
  • 返回值类型为dispatch_queue_t

在通过函数获取Dispatch Queue 以及其他名称中含有create 的API 生成的对象时,有必要通过dispatch_retain函数持有,并在不需要时通过dispatch_release函数释放.

注意不能过多生成Serial Dispatch Queue

2.3 Main Dispatch Queue/Global Dispatch Queue

获取系统标准提供的Dispatch Queue。
Main Dispatch Queue:是在主线程中执行的Dispatch Queue,只有一个。追加到其中的线程会在主线程的RunLoop中执行,因此要将用户界面的操作追加到其中。
Global Dispatch Queue:所有应用程序都能使用的Dispatch Queue。有四个执行优先级:高、默认、低、后台。在向其中追加处理时要选择对应优先级的Global Dispatch Queue。

表2 系统提供的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的获取
1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t myDispatchQueue;
//Main Dispatch Queue的获取
myDispatchQueue = dispatch_get_main_queue();
//Global Dispatch Queue (高优先级)的获取
myDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//Global Dispatch Queue (默认优先级)的获取
myDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//Global Dispatch Queue (低优先级)的获取
myDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
//Global Dispatch Queue (高优先级)的获取
myDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

对于Main Dispatch Queue 和Global Dispatch Queue 执行dispatch_retain 函数和dispatch_release 函数不会引起任何变化, 也不会有任何问题。

使用Main Dispatch Queue 和Global Dispatch Queue示例
1
2
3
4
5
6
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//可并行执行的处理
dispatch_async(dispatch_get_main_queue(), ^{
//只能在主线程中执行的处理
})
})

2.4 dispatch_set_target_queue

变更dispatch_queue_create生成的Dispatch Queue的优先级。
或者作成Dispatch Queue的执行阶层(可用来防止多个Serial Dispatch Queue之间并行处理)。

dispatch_queue_create生成的Dispatch Queue的优先级与默认的Global Dispatch Queue优先级相同。

1
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueLow);
  • 第一个参数:要变更优先级的Dispatch Queue。
  • 第二个参数:与要使用的执行优先级相同优先级的Dispatch Queue。

    2.5 dispatch_after

    在指定的时间追加处理到Dispatch Queue。

1
2
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{···});
  • 第一个参数:指定的时间。
  • 第二个参数:要追加到的Dispatch Queue。
  • 第三个参数:要执行处理的Block(用dispatch_time()或者dispatch_walltime()生成)。

dispatch_time函数能够获取从第一个参数指定的时间开始,到第二个参数指定的时间(默认为纳秒)后的时间。ull表示“unsigned long long”,NSEC_PER_SEC表示以秒为单位,NSEC_PER_MSEC表示以毫秒为单位。

dispatch_walltime()函数用于计算绝对时间。

2.6 Dispatch Group

追加到Dispatch Queue 中的多个处理全部结束后想执行结束处理。

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();//生成group

dispatch_group_async(group, queue, blk0);//将blk追加到queue,将queue追加到group
dispatch_group_async(group, queue, blk1);
dispatch_group_async(group, queue, blk2);

//group中的所有queue处理完成后通知main_queue进行处理。使用dispatch_group_wait也可。
dispatch_group_notify(group, dispatch_get_main_queue, blkDone);
dispatch_release(group);//释放group

2.7 dispatch_barrier_async

可实现高效率的数据库访问和文件访问。
同Concurrent Dispatch Queue一起使用,当该queue上的并行执行的处理全部结束后,再将指定的处理追加到queue中,指定的处理执行完成之后,再开始并行执行。

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_queue_create(”com.example.gcd.ForBarrier”, DISPATCH_QUEUE_CONCURRENT);

//blk0和blk1并行读取完成之后,执行blk进行写操作,写入完成之后blk2和blk3再次并行读取
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_barrier_async(queue, blk_for_writing);//写操作
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);

dispatch_release(queue);

2.8 dispatch_sync

dispatch_async:将指定的Block非同步地追加到指定的Dispatch Queue中,不做任何等待。
dispatch_sync:将指定的Block同步地追加到指定的Dispatch Queue中,Block执行完成之前,dispatch_sync一直等待。容易出现死锁

2.9 dispatch_apply

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

1
2
3
4
5
6
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index){//0-9分别作为参数传入index
NSLog(@"%zu", index);
});
NSLog(@"done");
//执行结果为:4 1 0 3 5 2 6 8 9 7 done

由于dispatch_apply同dispatch_sync一样,会等待执行结束,因此推荐在dispatch_async 函数中非同步地执行dispatch_apply 函数。

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_async(globalQueue, ^{

dispatch_apply([array count], globalQueue, ^(size_t index){
//并行处理array中的对象
NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
});//dispatch_apply全部处理结束,才会继续执行下面代码

//在Main Dispatch Queue中非同步执行界面更新
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"done");
});
});

2.10 dispatch_suspend / dispatch_resume

在追加处理的过程中,有时希望不执行己追加的处理。例如演算结果被Block 截获时, 一些处理会对这个演算结果造成影响。
dispatch_suspend挂起指定的Dispatch Queue。
dispatch_resume恢复指定的Dispatch Queue。

2.11 Dispatch Semaphore

Dispatch Semaphore是持有计数的信号,用来对多线程资源进行更细粒度的控制。
计数为0时等待,大于等于1时将其减去1而不等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//创建,参数表示计数的初始值,也需要使用dispatch_retain和dispatch_release来进行内存管理
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

//使用方法一:等待指定时间
long result = dispatch_semaphore_wait(semaphore, time);
if(result == 0){
//计数不为0,可执行需要进行排他控制的处理
} else {
//计数为0,因此在达到指定时间为止待机
}

//使用方法二:永久等待,直到计数大于等于1
//一直等待,直到计数大于等于1,将计数设为0,并返回
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//计数为0,不会有其他线程进行操作,可放心处理
[array addObject: [NSNumber numberWithlnt:i]];
//将计数加1,通知其他线程可进行处理
dispatch_semaphore_signal(semaphore) ;

dispatch_release(semaphore);

2.12 dispatch_once

保证应用程序只执行一次指定处理。
生成单例对象使用。

1
2
3
4
static dispatch_once_t pred;
dispatch_once(&pred, ^{
//初始化
});

2.13 Dispatch I/0

多线程并行读取大文件。

3.GCD的实现

3.1 Dispatch Queue

表3 用于实现Dispatch Queue 而使用的软件组件
组件名称 提供计数
libdispatch Dispatch Queue
Libc (pthreads) pthread_workqueue
XNU内核 workqueue

  我们使用的GCD的API全部包含在libdispatch 库中的C 语言函数。
  Dispatch Queue 通过结构体和链表,被实现为FIFO 队列。FlFO 队列管理是通过dispatch_async 等函数所追加的Block 。
  Block 先加入Dispatch Continuation 这一dispatch_continuation_t类型结构体中,然后再加入FIFO 队列
  该Dispatch Continuation 相当于执行上下文,用于记忆Block 所属的Dispatch Group 和其他一些信息。

Global Dispatch Queue 有如下8 种:

  • Global Dispatch Queue (High Priority )
  • Global Dispatch Queue (Default Priority )
  • Global Dispatch Queue (Low Priority)
  • Global Dispatch Queue (Background Priority )
  • Global Dispatch Queue (High Overcommit Priority )
  • Global Dispatch Queue (Default Overcommit Priority )
  • Global Dispatch Queue (Low Overcommit Priority )
  • Global Dispatch Queue (Background Overcommit Priority)
      附有Overcommit 的Global Dispatch Queue 使用在Serial Dispatch Queue 中,不管系统状态如何,都会强制生成线程的Dispatch Queue。
      这8 种Global Dispatch Queue 各使用1个pthread_workqueue。GCD 初始化时,使用pthread_workqueue_create_np 函数生成pthread_workqueue。
      pthread_workqueue 包含在Libc 提供的pthreads API 中。其使用bsdthread_registerworkq_open 系统调用,在初始化XNU 内核的workqueue 之后获取workqueue 信息。

XNU 内核持有4 种workqueue.

  • WORKQUEUE_HIGH_PRIOQUEUE
  • WORKQUEUE_DEFAULT_PRIOQUEUE
  • WORKQUEUE_LOW_PRIOQUElJE
  • WORKQUEUE_BG_PRJOQUEUE

3.1.1 Dispatch Queue 执行的大概过程

  1. 当在Global Dispatch Queue 中执行Block 时libdispatch 从Global Dispatch Queue 自身的FlFO 队列中取出Dispatch Continuation,调用pthread_workqueue_additem_np函数,使用workq_kernreturn系统调用,通知workqueue将该Global Dispatch Queue 自身、符合其优先级的workqueue 信息以及为执行Dispatch Continuation 的回调函数等传递给参数。通知XNU 内核基于系统状态判断是否要生成线程。如果是Overcommit优先级的Global Dispatch Queue, workqueue 则始终生成线程。
  2. workqueue 的线程执行pthread_workqueue函数,该函数调用libdispatch 的回调函数。在该回调函数中执行加入到Dispatch Continuation 的Block。
  3. Block 执行结束后,进行通知Dispatch Group 结束、释放Dispatch Continuation 等处理,开始准备执行加入到Global Dispatch Queue 中的下一个Block。
    ![图3.2 Global Dispatch Queue 与pthread_workqueue、workqueue 的关系](https://github.com/MrConfused/img/blob/master/IOS/OC/OC%E9%AB%98%E7%BA%A7%E7%BC%96%E7%A8%8B-IOS%E4%B8%8EOS_X%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%92%8C%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/%E5%9B%BE3.2%20Global%20Dispatch%20Queue%20%E4%B8%8Epthread_workqueue%E3%80%81workqueue%20%E7%9A%84%E5%85%B3%E7%B3%BB.png?raw=true)
    图3.2 Global Dispatch Queue 与pthread_workqueue、workqueue 的关系

3.2 Dispatch Source

Dispatch Source是BSD系内核惯有功能kqueue 的包装。
kqueue 是在XNU 内核中发生各种事件时,在应用程序编程方执行处理的技术。
其CPU 负荷非常小,尽量不占用资源。kqueue 可以说是应用程序处理XNU 内核中发生的各种事件的方法
中最优秀的一种。
Dispatch Source 与Dispatch Queue 不同,是可以取消的。而且取消时必须执行的处理可指定为回调用的Block 形式。

表4 Dispatch Source 可处理的事件
名称 内容
DISPATCH_SOURCE_TYPE_DATA_ADD 变量增加
DISPATCH_SOURCE_TYPE_DATA_OR 变量OR
DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送
DISPATCH SOURCE_TYPE_MACH_RECV MACH端口接收
DISPATCH_SOURCE_TYPE_PROC 检测到与进程相关的事件
DISPATCH SOURCE TYPE_READ 可读取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接收信号
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件系统有变更
DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像
事件发生时,在指定的Dispatch Queue 中可执行事件的处理。
使用DISPATCH_SOURCE_TYPE_READ,异步读取文件映像。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
__block size_t total = 0;
size_t size = 要读取的字节数;
char *buff = (char *)malloc(size);
fcnt1(sockfd, F_SETFL, 0_NONBLOCK);//设定为异步映像
//获取用于追加事件处理的queue
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//基于READ事件,作成Dispatch Source
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, sockfd, 0, queue);

//指定发生READ事件时执行的处理
dispatch_source_set_event_handler(source, ^{
size_t available = dispatch_source_get_data(source);//获取可读取的字节数
int length = read(sockfd, buff, available);//从映像中读取
if(length < 0){ //错误处理
dispatch_source_cancel(source);
}
total += length;
if(total == size){
/*
* buff的处理
*/
dispatch_source_cancel(source);//处理结束,取消Dispatch Source
}
});

//指定取消Dispatch source 时的处理
dispatch_source_set_cancel_handler(source, ^{
free(buff);
close(sockfd);
dispatch_release(source);//释放放Dispatch Source自身
});

dispatch_resume(source); //启动Dispatch Source
使用DISPATCH_SOURCE_TYPE_TJMER 的定时器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//指定DIS PATCH_SOURCE_TYPE_TIMER ,作成Dispatch Source。
//在定时器经过指定时间的设定Main Dispatch Queue 为追加处理的Dispatch Queue
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());

dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, l5ull * NSEC_PER_SEC), //将定时器设定为15秒后。
DISPATCH_TIME_FOREVER, //不指定为重复。
lull * NSEC PER SEC);//允许迟延1秒。

//指定定时器指定时间内执行的处理
dispatch_source_set_event_handler(timer, ^{
NSLog(@"wakeup!");
dispatch_source_cancel(timer);//取消Dispatch Source
});

//指定取消Dispatch Source 时的处理
dispatch_source_set_cancel_handler(source, ^{
NSLog(@"canceled!");
dispatch_release(timer);
});

dispatch_resume(source); //启动Dispatch Source
0%