Threads(六)GCD - Dispatch Sources

每当与底层系统交互时,你都必须为该任务花费大量时间做好准备。调用内核层或其他系统层涉及到上下文的更改,与在你自己的进程中发生的调用相比,该更改相当昂贵。因此,许多系统库提供异步接口,允许你的代码向系统提交请求,并在处理该请求的同时继续执行其他工作。Grand Central Dispatch基于此一般行为,允许你提交请求,并使用blocksdispatch queues将结果报告回代码。

Dispatch Sources

dispatch source是一种基本数据类型,用于协调特定低级系统事件的处理。Grand Central Dispatch支持以下类型的dispatch sources

  • Timer dispatch sources 生成定期通知。

  • UNIX 信号到达时,Signal dispatch sources(信号调度源)会通知你。

  • descriptor sources 会通知你各种基于文件和套接字的操作,例如:

1
2
3
4
1、当数据可供读取时
2、何时可以写入数据
3、在文件系统中删除,移动或重命名文件时
4、文件元信息更改时
  • Process dispatch sources通知你与进程相关的事件,例如:
1
2
3
1、进程退出时
2、当进程发出`fork`或`exec`类型的调用时
3、当信号传递到进程时
  • Mach port dispatch sources会通知你与Mach相关的事件。

  • Custom dispatch sources是你自己定义并触发的源。

Dispatch sources取代了通常用于处理与系统相关事件的异步回调函数。配置dispatch source时,可以指定 要监视的事件 以及 用于处理这些事件的dispatch queue和代码。可以使用block对象或函数指定代码。当感兴趣的事件到达时,dispatch source将你的block或函数提交到指定的dispatch queue执行。

与手动提交到queue的任务不同,dispatch sources为你的应用程序提供了连续的事件源。dispatch source将一直附加到其dispatch queue,直到你明确取消它为止。附加时,每当发生相应事件时,它都会将其关联的任务代码提交给dispatch queue。某些事件(例如timer events)以固定的间隔发生,但大多数事件仅在特定条件出现时才偶尔发生。因此,dispatch sources``retain其关联的dispatch queue,以防止在事件可能仍处于挂起状态时过早地释放该queue

为了防止事件在dispatch queue中积压,dispatch sources实现了event coalescing scheme(事件合并方案)。如果新事件在 前一个事件的event handlerqueue并执行 之前到达,则dispatch source将新事件中的数据与旧事件中的数据合并。根据事件的类型,合并可能会替换旧事件或更新其持有的信息。例如,signal-based dispatch source(基于信号的调度源)仅提供有关最新信号的信息,而且还报告自从上次调用event handler(事件处理程序)以来已传递了多少信号总数。

创建 Dispatch Sources

创建dispatch source包括创建 事件源 和dispatch source本身。事件的 source 是处理事件所需的任何本地数据结构。例如,对于descriptor-based dispatch source(基于描述符的调度源),你将需要打开描述符;对于process-based source(基于进程的源),则需要获取目标程序的进程ID。有了event source(事件源)后,就可以按照以下方式创建相应的dispatch source

1、使用 dispatch_source_create 函数创建dispatch source

2、配置dispatch source

  • event handler分配给dispatch source;
  • 对于timer sources,请使用 dispatch_source_set_timer 函数设置定时器信息。

3、为dispatch source分配一个cancellation handler(取消处理程序) (这一步可选);

4、调用dispatch_resume函数开始处理事件;

由于 dispatch sources 在使用前需要进行一些额外配置,因此dispatch_source_create 函数以挂起状态返回 dispatch sources。挂起时,dispatch source将接收事件,但不处理它们。这使你有时间安装 event handler 并执行处理实际事件所需的任何其他配置。

编写和安装 Event Handler

要处理 dispatch source 生成的事件,必须定义一个event handle(事件处理程序)来处理这些事件。event handle是你使用dispatch_source_set_event_handlerdispatch_source_set_event_handler_f 函数安装在 dispatch source 上的函数或block对象。当事件到达时,dispatch source将你的event handler提交到指定的dispatch queue进行处理。

event handler的主体负责处理所有到达的事件。如果你的 event handler 已经排队并且正在等待处理事件,当一个新事件到来时,则dispatch source将合并这两个事件。event handler通常只查看最新事件的信息,但根据dispatch source的类型,event handler也可以能够获取有关发生并合并的其他事件的信息。如果一个或多个新事件在event handler开始执行后到达,则 dispatch source 将保留这些事件,直到当前event handler完成执行为止。此时,它将使用新事件再次将event handler提交到队列。

1
2
3
4
5
// 基于 block 的事件处理程序不带参数,也没有返回值。
void (^dispatch_block_t)(void)

// 基于函数的事件处理程序采用单个上下文指针,其中包含dispatch source对象,并且不返回值
void (*dispatch_function_t)(void *)

event handler内部,可以从dispatch source本身获取有关给定事件的信息。尽管将function-based event handlers(基于函数的事件处理程序)作为参数传递了指向dispatch source的指针,但是block-based event handlers(基于块的事件处理程序)必须自己捕获该指针。可以通过正常引用包含dispatch source的变量来对blocks执行此操作。 例如,以下代码片段捕获了在blocks范围之外声明的source变量。

1
2
3
4
5
6
7
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
myDescriptor, 0, myQueue);
dispatch_source_set_event_handler(source, ^{
// 从捕获的源变量中获取一些数据。来自父上下文。
size_t estimated = dispatch_source_get_data(source);
});
dispatch_resume(source);

block内部捕获变量通常是为了获得更大的 flexibility(灵活性) 和 dynamism(动态性)。当然,默认情况下,捕获的变量在block内是只读的。尽管blocks功能支持了在特定情况下修改捕获的变量,但不应在与dispatch source关联的event handlers中尝试这样做。Dispatch sources始终异步执行其event handlers,因此捕获的任何变量的定义范围可能在event handler执行时就消失了。

如下列出了可以从event handler代码中调用以获取事件信息的函数。

  • dispatch_source_get_handle

此函数返回dispatch source管理的基础系统数据类型。

对于descriptor dispatch source,此函数返回一个int类型,其中包含与dispatch source关联的descriptor(描述符)。

对于signal dispatch source,此函数返回一个int类型,其中包含最近事件的信号编号。

对于process dispatch source,此函数返回被监视进程的pid_t数据结构。

对于Mach port dispatch source,此函数返回mach_port_t数据结构。

对于其他dispatch sources,此函数返回的值未定义。

  • dispatch_source_get_data

此函数返回与事件关联的所有挂起数据。

对于从文件读取数据的descriptor dispatch source,此函数返回可读取的字节数。

对于将数据写入文件的descriptor dispatch source,如果有可用的写入空间,则此函数返回正整数。

对于监视文件系统活动的descriptor dispatch source,此函数返回一个常量,指示发生的事件的类型。有关常量的列表,请参见dispatch_source_vnode_flags_t枚举类型。

对于process dispatch source,此函数返回一个常量,指示发生的事件的类型。有关常量的列表,请参见dispatch_source_proc_flags_t枚举类型。

对于Mach port dispatch source,此函数返回一个常量,指示发生的事件的类型。有关常量的列表,请参见dispatch_source_machport_flags_t枚举类型。

对于custom dispatch source,此函数返回根据现有数据创建的新数据值,并将新数据传递给dispatch_source_merge_data函数。

  • dispatch_source_get_mask

此函数返回用于创建dispatch source的事件标志。

对于process dispatch source,此函数返回dispatch source接收的事件的掩码。有关常量的列表,请参见dispatch_source_proc_flags_t枚举类型。

对于具有发送权限的Mach port dispatch source,此函数返回所需事件的掩码。 有关常量的列表,请参见dispatch_source_mach_send_flags_t枚举类型。

对于自定义OR dispatch source,此函数返回用于合并数据值的掩码。

安装 Cancellation Handler

Cancellation handlers(取消处理程序)用于在release dispatch source之前对其进行清理。对于大多数类型的dispatch sourcescancellation handlers是可选的,并且仅当你有一些与dispatch source相关联的自定义行为也需要更新时,才需要Cancellation handlers。但是,对于使用descriptorMach portdispatch sources,必须提供cancellation handler以关闭descriptorrelease Mach port。否则,由于代码或系统其他部分无意识地重用了那些结构,可能导致代码中的细微错误。

你可以随时安装cancellation handler,但通常在创建dispatch source时会这样做。你可以使用dispatch_source_set_cancel_handlerdispatch_source_set_cancel_handler_f函数来安装cancellation handler,具体取决于你要在实现中使用block对象还是函数。

下面的示例显示一个简单的cancellation handler,它关闭为dispatch source打开的descriptor(描述符)。fd变量是包含descriptor的捕获变量。

1
2
3
dispatch_source_set_cancel_handler(mySource, ^{
close(fd); // Close a file descriptor opened earlier.
});

更改Target Queue

尽管在创建dispatch source时指定了要在其上运行事件和cancellation handlers(取消处理程序)的队列,但可以使用dispatch_set_target_queue函数随时更改该queue。你可以这样做来更改处理dispatch source事件的优先级。

更改dispatch sourcequeue是异步操作,dispatch source 会尽最大努力尽快进行更改。如果event handler(事件处理程序)已经在队列中并等待处理,它将在前一个queue上执行。但是,在你进行更改时 到达的其它事件 可以在任一queue中进行处理。

将自定义数据 与 Dispatch Source 相关联

Grand Central Dispatch中的许多其他数据类型一样,你可以使用dispatch_set_context函数将自定义数据与dispatch source相关联。可以使用上下文指针存储event handler处理事件所需的任何数据。如果确实在上下文指针中存储了任何自定义数据,则还应该安装cancellation handler,以便在不再需要dispatch sourcerelease该数据。

如果使用blocks实现event handler(事件处理程序),则还可以捕获局部变量并在block-based code(基于块的代码)中使用它们。尽管这可以减少将数据存储在dispatch source的上下文指针中的需要,但你应始终谨慎使用此功能。因为dispatch sources可能在你的应用程序中长期存在,所以在捕获包含指针的变量时应格外小心。如果可以随时释放指针所指向的数据,则应复制数据或保留数据以防止发生这种情况。无论哪种情况,你都需要提供一个cancellation handler,以便以后释放数据。

Dispatch Sources 的内存管理

像其他dispatch objects一样,dispatch sources也是reference counted data types(引用计数的数据类型)。dispatch source的初始引用计数为1,可以使用dispatch_retaindispatch_release函数进行保留和释放。当queue的引用计数达到零时,系统将自动释放dispatch source数据结构。

由于它们的使用方式,可以在dispatch sources本身内部或外部管理dispatch sources的所有权。有了外部所有权,另一个对象或代码段就拥有了dispatch source的所有权,并负责在不再需要时释放它。对于内部所有权,dispatch source拥有自己,并负责在适当的时间释放自己。尽管外部所有权很常见,但是在想要创建自主dispatch source,并让它管理代码的某些行为 而无需任何进一步交互的情况下,可以使用 内部所有权。例如,如果dispatch source被设计为响应单个全局事件,则可以让它处理该事件然后立即退出。

Dispatch Source 示例

以下各节说明如何创建和配置一些更常用的调度源。

创建 Timer

Timer dispatch sources以定期、基于时间的时间间隔生成事件。可以使用timer来启动需要定期执行的特定任务。例如,游戏和其他图形密集型应用程序可能使用计时器来启动屏幕或动画更新。还可以设置一个timer,并使用产生的事件来检查频繁更新的服务器上的新信息。

所有timer dispatch sources都是interval timers(间隔计时器),即一旦创建,它们就会以你指定的间隔传递常规事件。创建timer dispatch source时,必须指定的值之一是leeway(回旋值),以使系统对timer events的期望精度有所了解。Leeway值使系统在 管理电源 和 唤醒内核 方面具有一定的灵活性。例如,系统可以使用leeway值来提前或延迟触发时间,并使它与其他系统事件更好地保持一致。因此,你应该尽可能为自己的timers指定一个leeway值。

注意:即使你指定的leeway值为0,也永远不要期望timer以你要求的精确纳秒触发。该系统会尽力满足您你需求,但无法保证确切的触发时间。

当计算机进入sleep状态时,所有timer dispatch sources都将被挂起。当计算机唤醒时,这些timer dispatch sources也会自动唤醒。根据timer的配置,这种性质的暂停可能会影响你的timer下次触发的时间。如果你使用dispatch_time函数或DISPATCH_TIME_NOW常量设置timer dispatch source,则timer dispatch source将使用default system clock(默认系统时钟)来确定何时触发。但是,当计算机处于休眠状态时,default clock不会提前。相比之下,当使用dispatch_walltime函数设置timer dispatch source时,timer dispatch source会将其触发时间跟踪到挂钟时间。后一种选项通常适用于触发间隔相对较大的timers,因为它可以防止事件时间之间的偏移过大。

创建一个timer dispatch source

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
// 或者在主队列 dispatch_get_main_queue()
dispatch_queue_t queue = dispatch_queue_create("com.example.MyQueue", NULL);

// 1、创建 timer
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, queue);
if (timer) {
/* 2、配置timer
* 参数2:表示timer开始触发的事件。由于此处timer每30秒触发一次,时间间隔较大,因此使用 dispatch_walltime
* 参数3:表示timer多少秒触发一次
* 参数4:表示timer的 leeway 值
*/
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 30ull * NSEC_PER_SEC, 1ull * NSEC_PER_SEC);

// 3、设置timer的事件处理程序
dispatch_source_set_event_handler(timer, ^{
NSLog(@"%@", [NSThread currentThread]);
});

// 4、让timer开始处理事件。dispatch_suspend 与 dispatch_resume的作用相反,用于让timer暂停
dispatch_resume(timer);
}

// 5、存储timer的引用,避免被立即释放
self.timer = timer;

// 打印:
// <NSThread: 0x600002363940>{number = 3, name = (null)}

尽管创建timer dispatch source是接收基于时间的事件的主要方式,但也可以使用其他选项。如果要在指定的时间间隔后执行一次block,则可以使用dispatch_afterdispatch_after_f函数。该函数的行为与dispatch_async函数非常相似,不同之处在于,它允许你指定将block交到queue的时间值。可以根据需要将时间值指定为相对或绝对时间值。

1
2
3
4
5
6
7
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", [NSThread currentThread]);
});

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(@"%@", [NSThread currentThread]);
});

Descriptor读取数据

要从file(文件)或socket(套接字)读取数据,必须打开file(文件)或socket(套接字)并创建DISPATCH_SOURCE_TYPE_READ类型的dispatch source。你指定的event handler应该能够读取和处理file descriptor(文件描述符)的内容。对于file,这相当于读取文件数据(或该数据的子集)并为你的应用程序创建适当的数据结构。对于network socket(网络套接字),这涉及处理新接收到的网络数据。

每当读取数据时,都应始终将descriptor(描述符)配置为使用non-blocking operations(非阻塞操作)。虽然可以使用dispatch_source_get_data函数查看有多少数据可供读取,但是该函数返回的数字可能会在你进行调用的时间 与 你实际读取数据的时间之间发生变化。如果基础文件被截断或发生网络错误,则从当前线程中的descriptor(描述符)中读取数据可能会使event handler在执行过程中暂停,并阻止dispatch queue调度其他任务。对于serial queue,这可能会使队列死锁,甚至对于concurrent queue,这也会减少可以启动的新任务的数量。

如下示例,将dispatch source配置为从文件读取数据:

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
34
35
36
37
38
39
40
41
42
43
dispatch_source_t ProcessContentsOfFile(const char* filename)
{
// 准备文件开始读取
int fd = open(filename, O_RDONLY);
if (fd == -1)
return NULL;
fcntl(fd, F_SETFL, O_NONBLOCK); // 为了确保dispatch queue在没有数据读取时不会不必要地阻塞,使用fcntl函数来避免阻塞读取操作

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
fd, 0, queue);
if (!readSource)
{
close(fd);
return NULL;
}

// 安装 event handler
dispatch_source_set_event_handler(readSource, ^{
size_t estimated = dispatch_source_get_data(readSource) + 1;
// 将数据读取到文本缓冲区中
char* buffer = (char*)malloc(estimated);
if (buffer)
{
ssize_t actual = read(fd, buffer, (estimated));
Boolean done = MyProcessFileData(buffer, actual); // 通过自定义函数处理数据

// 完成后释放缓冲区
free(buffer);

// 没有数据了,取消source
if (done)
dispatch_source_cancel(readSource);
}
});

// 安装 cancellation handler,确保在读取数据后关闭文件描述符
dispatch_source_set_cancel_handler(readSource, ^{close(fd);});

// 开始读取文件
dispatch_resume(readSource);
return readSource;
}

将数据写入 Descriptor

将数据写入file(文件)或socket(套接字)的过程与读取数据的过程非常相似。配置用于写操作的descriptor(描述符)之后,将创建类型为DISPATCH_SOURCE_TYPE_WRITEdispatch source。一旦创建dispatch source,系统将调用你的event handler,使它有机会开始将数据写入file(文件)或socket(套接字)。完成数据写入后,请使用dispatch_source_cancel函数取消dispatch source

每当写入数据时,都应始终将file descriptor(文件描述符)配置为使用non-blocking operations(非阻塞操作)。尽管可以使用dispatch_source_get_data函数查看有多少空间可供写入,但是该函数返回的值仅是建议性的,并且可能在 调用时间 和 实际写入数据的时间 之间发生变化。如果发生错误,将数据写入blockfile descriptor可能会使event handler执行过程中暂停,并阻止dispatch queue调度其他任务。对于serial queue,这可能会使队列死锁,甚至对于concurrent queue,这也会减少可以启动的新任务的数量。

监控 File-System 对象

如果要监控file system object(文件系统对象)的更改,则可以设置类型为DISPATCH_SOURCE_TYPE_VNODEdispatch source。当文件被删除,写入或重命名时,可以使用这种类型的dispatch source来接收通知。当文件的特定类型的元信息(例如文件的大小和链接数)发生变化时,你也可以使用它来发出警报。

注意:当source本身正在处理事件时,为dispatch source指定的file descriptor(文件描述符)必须保持打开状态。

监控信号

UNIX signals允许从其域外部对应用程序进行操作。应用程序可以接收许多不同类型的signals(信号),范围从不可恢复的错误(例如非法指令)到有关重要信息的通知(例如子进程退出时)。传统上,应用程序使用sigaction函数来安装signal handler function,该功能在signals到达时立即对signals同步处理。如果你只想收到signals到达的通知,而实际上却不想处理该signals,则可以使用signal dispatch source来异步处理signals

Signal dispatch sources(信号调度源)不能替代你使用sigaction函数安装的synchronous signal handlers(同步信号处理程序)。Synchronous signal handlers实际上可以捕获signal(信号)并阻止它终止你的应用程序。Signal dispatch sources允许你仅监视signal(信号)的到达。此外,你不能使用signal dispatch sources来检索所有类型的signal(信号)。具体来说,不能使用它们来监视SIGILLSIGBUSSIGSEGV信号。

因为signal dispatch sources(信号调度源)在dispatch queue上异步执行,所以它们不受与synchronous signal handlers相同的限制。例如,你可以从signal dispatch source的事件处理程序中调用的函数没有任何限制。这种灵活性的权衡是,在signal到达的时间与dispatch source的事件处理程序被调用之间的延迟可能会有所增加。

如果你正在为自定义框架开发代码,则使用signal dispatch sources的优点是你的代码可以独立于与其链接的任何应用程序监控信号。Signal dispatch sources不会干扰应用程序可能已安装的其他dispatch sources或任何同步信号处理程序。

监控进程

process dispatch source使你可以监视特定进程的行为并做出适当响应。父进程可以使用这种类型的dispatch source来监视它创建的任何子进程。例如,父进程可以使用它来监视子进程的死亡。同样,子进程可以使用它来监视其父进程并在父进程退出时退出。

清单4-6显示了安装调度源以监视父进程终止的步骤。 当父进程死亡时,调度源将设置一些内部状态信息,以使子进程知道应该退出。 (您自己的应用程序将需要实现MySetAppExitFlag函数,以设置适当的终止标志。)由于调度源是自动运行的,因此拥有自己的资源,因此它在预期程序关闭时也会取消并释放自身。

取消 Dispatch Source

Dispatch sources 将保持活动状态,直到你使用 dispatch_source_cancel 函数显式取消它为止。取消dispatch source将停止新事件的传递,并且无法撤消。 因此,通常可以取消dispatch source,然后立即释放它,如下所示:

1
2
3
4
5
void RemoveDispatchSource(dispatch_source_t mySource)
{
dispatch_source_cancel(mySource);
dispatch_release(mySource);
}

dispatch source是异步操作。尽管在调用dispatch_source_cancel函数之后不会处理任何新事件,但是已由dispatch source处理的事件将继续被处理。完成所有最终事件的处理后,如果存在dispatch source,则dispatch source将执行其cancellation handler(取消处理程序)。

cancellation handler(取消处理程序)是你 释放内存 或 清除代表dispatch source获取的任何资源的机会。如果你的dispatch source使用descriptor(描述符)或mach port,则必须提供cancellation handler(取消处理程序)以关闭descriptor(描述符)或在发生取消时销毁port。其他类型的dispatch sources不需要cancellation handlers,但是如果将任何内存或数据与dispatch source相关联,则仍应提供cancellation handlers。例如,如果将数据存储在dispatch source的上下文指针中,则应提供一个。

挂起和恢复 Dispatch Sources

可以使用dispatch_suspenddispatch_resume方法临时挂起和恢复dispatch source事件的传递。这些方法增加和减少dispatch object的挂起计数。因此,在恢复事件传递之前,必须平衡对dispatch_suspend的每个调用和匹配对dispatch_resume的调用。

当你挂起dispatch source时,该dispatch source被挂起时发生的任何事件都将被累积,直到恢复queue为止。当queue恢复时,不是传递所有事件,而是在传递之前将事件合并为单个事件。例如,如果您正在监视文件中的名称更改,则传递的事件将仅包括姓氏更改。以这种方式合并事件可以防止它们在队列中堆积,并在工作恢复时使您的应用程序不堪重负。

学习博客

Dispatch Sources

Summerrose’s Blog * Dispatch Source

多线程之GCD底层原理篇

文章作者: Czm
文章链接: http://yoursite.com/2020/10/23/Threads-%E5%85%AD-GCD-Dispatch-Sources/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm