每当你与底层系统打交道时,必须准备好该任务可能需要花费大量的时间。对内核或其他系统层的调用涉及到上下文的改变,与发生在进程中的调用相比,这种改变是相当昂贵的。因此,许多系统库提供了异步接口,允许你的代码向系统提交一个请求,并在处理该请求时继续做其他工作。Grand Central Dispatch建立在这种一般行为的基础上,允许你提交请求,并使用block和调度队列将结果反馈给你的代码。
关于调度源
调度源是一个基本的数据类型,它协调特定的底层系统事件的处理。Grand Central Dispatch支持以下类型的调度源:
- 计时器调度源产生定期通知。
- 信号调度源在UNIX信号到达时发出通知。
- 描述符源通知你各种基于文件和套接字的操作,例如:
- 当数据可供读取时;
- 当可以写入数据时;
- 当文件在文件系统中被删除、移动或重命名时;
- 当文件元信息发生变化时;
- 进程调度源通知你与进程有关的事件,如:
- 当一个进程退出时;
- 当一个进程发出一个
fork
或exec
类型的调用时; - 当一个信号被传递给进程时;
- 机器端口调度源通知与机器有关的事件。
- 自定义调度源可以由自己定义和触发。
调度源取代通常用于处理系统相关事件的异步回调函数。当你配置一个调度源时,指定你想监控的事件和调度队列,以及用来处理这些事件的代码。你可以使用block对象或函数指定你的代码。当一个感兴趣的事件到来时,调度源会将你的block或函数提交给指定的调度队列来执行。
与手动提交到队列的任务不同,调度源为程序提供了一个持续的事件源。在你明确取消它之前,一个调度源一直连接到它的调度队列。在连接期间,每当相应的事件发生时,它都会向调度队列提交其相关的任务代码。有些事件,如定时器事件,会定期发生,但大多数事件只是在特定条件出现时零星地发生。出于这个原因,调度源保留其相关的调度队列,以防止它在事件可能仍在等待时被过早释放。
为了防止事件积压在调度队列中,调度源实施了一个事件合并(coalescing)方案。如果一个新的事件在前一个事件的handler被取消排队并执行之前到达,调度源就会将新的事件数据与旧事件的数据合并起来。根据事件的类型,合并可能会取代旧事件或更新其持有的信息。例如,一个基于信号的调度源只提供关于最近的信号信息,但也报告自上次调用事件handler以来,总共有多少信号被传递。
创建调度源
创建一个调度源包括创建事件源和调度源本身。事件源是处理这些事件所需的任何本地数据结构。例如,对于一个基于描述符的调度源,你需要打开描述符,而对于一个基于进程的源,你需要获得目标程序的进程ID。当你有了你的事件源,你就可以按以下方法创建相应的调度源:
- 使用
dispatch_source_create
函数创建调度源。 - 配置调度源:
- 为调度源分配一个事件handler;可参阅Writing and Installing an Event Handler。
- 对于定时器源,使用
dispatch_source_set_timer
函数设置定时器信息;可参阅Creating a Timer。
- 可以选择给调度源分配一个取消handler;可参阅Installing a Cancellation Handler。
- 调用
dispatch_resume
函数开始处理事件;可参阅Suspending and Resuming Dispatch Sources。
由于调度源在使用前需要一些额外的配置,dispatch_source_create
函数在暂停状态下返回调度源。在暂停状态下,调度源接收事件但不处理它们。这使你有时间配置一个事件handler,并执行处理实际事件所需的其他配置。
下面的章节向你展示了如何配置调度源。关于展示如何配置特定类型的调度源的详细例子,可参阅Dispatch Source Examples。关于用来创建和配置调度源的函数的其他信息,可参阅Grand Central Dispatch (GCD) Reference。
编写和配置一个事件Handler
为了处理由调度源产生的事件,你必须定义一个事件handler来处理这些事件。事件handler是一个函数或block对象,用dispatch_source_set_event_handler
或dispatch_source_set_event_handler_f
函数将其配置在调度源上。当一个事件到来时,调度源会将事件handler提交给指定的调度队列进行处理。
你的事件handler的主体负责处理任何到达的事件。如果你的事件handler已经在队列中并等待处理一个事件,当一个新的事件到达时,调度源会将这两个事件合并起来。一个事件handler通常只看到最近的事件的信息,但根据调度源的类型,它也可以获得其他已经发生并被合并的事件的信息。如果一个或多个新的事件在事件handler开始执行后到达,调度源会保留这些事件,直到当前事件handler执行完毕。这时,它将事件handler与新的事件一起再次提交给队列。
基于函数的事件handler接受一个单一的上下文指针,包含调度源对象,并且不返回任何值。基于block的事件handler不接受参数,也没有返回值。
1 | // Block-based event handler |
在事件handler中,你可以从调度源本身获得关于给定事件的信息。尽管基于函数的事件handler被传递一个指向调度源的指针作为参数,但基于block的事件handler必须自己捕获这个指针。你可以通过正常引用包含调度源的变量来实现捕获指针。例如,下面的代码片段捕获了source
变量,它被声明在block的范围之外。
1 | dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, |
在block内捕获变量通常是为了获得更大的灵活性和动态性。当然,捕获的变量在block内默认为只读。尽管block功能提供了对特定情况下修改捕获变量的支持,但你不应该试图在与调度源相关的事件handler中这样做。调度源总是异步地执行它们的事件handler,所以当你的事件handler执行时,你捕获的任何变量的定义作用域很可能已经消失。关于如何在block内捕获和使用变量的更多信息,可参阅Blocks Programming Topics。
表4-1列出了可以从事件handler代码中调用的函数,以获取事件的信息。
Table 4-1 从调度源获取数据
dispatch_source_get_handle
:该函数返回调度源所管理的底层系统数据类型。
- 对于描述符调度源,该函数返回一个包含与调度源相关的描述符的
int
类型。 - 对于一个信号调度源,该函数返回一个
int
类型,包含最近事件的信号编号。 - 对于一个进程调度源,此函数返回一个
pid_t
数据结构,用于被监控的进程。 - 对于一个Mach端口调度源,此函数返回一个
mach_port_t
数据结构。 - 对于其他调度源,此函数返回的值是未定义的。
dispatch_source_get_data
:此函数返回与事件相关的任何未决(pending)数据。
对于从文件中读取数据的描述符调度源,该函数返回可供读取的字节数。
对于向文件写数据的描述符调度源,如果有空间可供写入,该函数返回一个正整数。
对于监视文件系统活动的描述符调度源,该函数返回一个
dispatch_source_vnode_flags_t
枚举,表示所发生的事件的类型。对于一个进程调度源,这个函数返回一个
dispatch_source_proc_flags_t
枚举,表示发生的事件类型。对于Mach端口调度源,此函数返回一个
dispatch_source_machport_flags_t
枚举,表示发生的事件类型。对于自定义调度源,此函数返回从现有数据和传递给
dispatch_source_merge_data
函数的新数据创建的新数据值。
dispatch_source_get_mask
:该函数返回用于创建调度源的事件标志。
对于一个进程调度源,该函数返回调度源所接收的事件的掩码(
dispatch_source_proc_flags_t
)。对于具有发送权限的Mach端口调度源,此函数返回所需事件的掩码(
dispatch_source_mach_send_flags_t
)。对于一个自定义OR调度源,此函数返回用于合并数据值的掩码。
关于如何为特定类型的调度源编写和配置事件handler的例子,可参阅Dispatch Source Examples。
配置取消Handler
取消handler用于在调度源被释放之前对其进行清理。对于大多数类型的调度源,取消handler是可选的,只有当你有一些与调度源绑定的自定义行为也需要被更新时才有必要。然而,对于使用描述符或Mach端口的调度源,你必须提供一个取消handler来关闭描述符或释放Mach端口。如果不这样做,这些结构体被你的代码和系统的其他部分无意地重用,可能会导致代码中出现微妙的错误。
可以在任何时候配置取消handler,但通常在创建调度源时进行配置。你可以使用dispatch_source_set_cancel_handler
或dispatch_source_set_cancel_handler_f
函数来配置取消handler,这取决于你想在实现中使用一个block对象还是一个函数。下面的例子显示了一个简单的取消handler,它关闭了一个为调度源打开的描述符。fd
变量是一个包含描述符的捕获变量。
1 | dispatch_source_set_cancel_handler(mySource, ^{ |
修改目标队列
尽管你在创建调度源时指定了运行事件和取消handler的队列,但你可以在任何时候使用 dispatch_set_target_queue
函数改变该队列。通过这样你可以改变调度源的事件处理的优先级。
修改调度源的队列是一个异步操作,调度源会尽最大努力尽快做出修改。如果一个事件handler已经在队列中并等待处理,它将在之前的队列中执行。然而,在你修改的时候,其他到达的事件可以在任一队列中处理。
关联自定义数据与调度源
像Grand Central Dispatch中的许多其他数据类型一样,你可以使用dispatch_set_context
函数来将自定义数据与调度源关联起来。可以使用上下文指针来存储事件handler在处理事件时需要的任何数据。如果你确实在上下文指针中存储了任何自定义数据,你也应该设置一个取消handler,以便在不再需要调度源时释放这些数据。
如果你使用block来实现你的事件handler,也可以捕获局部变量并在基于block的代码中使用它们。尽管这可能减轻了在调度源的上下文指针中存储数据的需要,但你应该始终谨慎地使用这一功能。因为调度源在程序中可能是长期存在的,在捕获包含指针的变量时应该小心。如果指针所指向的数据在任何时候都可能被释放,你应该复制该数据或保留它。在这两种情况下,你都需要配置一个取消handler来释放这些数据。
调度源的内存管理
像其他调度对象一样,调度源也是有引用计数的数据类型。一个调度源的初始引用计数为1,可以使用dispatch_retain
和dispatch_release
函数保留和释放。当一个队列的引用计数达到0时,系统会自动释放调度源的数据结构。
由于它们的使用方式,调度源的所有权可以由内部管理,也可以由外部管理。对于外部所有权,另一个对象或一段代码拥有调度源的所有权,并负责在不再需要它时将其释放。对于内部所有权,调度源持有自己,并负责在适当的时候释放自己。尽管外部所有权非常普遍,但在你想创建一个自主的调度源并让它管理你的代码的某些行为而不进行任何进一步的交互的情况下,你可能会使用内部所有权。例如,如果一个调度源被设计为响应一个单一的全局事件,你可能会让它处理该事件,然后立即退出。
调度源示例
下面的章节向你展示了如何创建和配置一些更常用的调度源。关于配置特定类型的调度源的更多信息,可参阅Grand Central Dispatch (GCD) Reference。
创建定时器
定时器调度源以定期、基于时间的间隔产生事件。你可以使用定时器来启动需要定期执行的特定任务。例如,游戏和其他图形密集型的程序可以使用定时器来启动屏幕或动画的更新。你也可以设置一个定时器并使用产生的事件来检查经常更新的服务器上的新信息。
所有的定时器调度源都是间隔性的定时器,也就是说,一旦创建,它们就会按照你指定的时间间隔定期发送事件。当你创建一个定时器调度源时,你必须指定的一个值是一个leeway值,以使系统知道定时器事件的所需精度。leeway值让系统在如何管理电源和唤醒内核方面有一定的灵活性。例如,系统可能会使用leeway值来提前或推迟启动时间,并使其与其他系统事件更好地协调。因此,你应该尽可能为你自己的定时器指定一个leeway值。
注意:即使你指定了一个0的leeway值,你也不应该期望定时器在你要求的精确纳秒处启动。系统会尽力满足你的需求,但不能保证精确的启动时间。
当计算机进入睡眠状态时,所有的定时器调度源都被暂停。当计算机唤醒时,这些定时器调度源也会被自动唤醒。根据定时器的配置,这种性质的暂停可能会影响定时器下一次触发的时间。如果你使用dispatch_time
函数或DISPATCH_TIME_NOW
常数来设置你的定时器调度源,定时器调度源会使用默认的系统时钟来决定何时启动。然而,当计算机处于睡眠状态时,默认的时钟不会前进。相比之下,当你使用dispatch_walltime
函数设置你的定时器调度源时,定时器调度源会跟踪其触发时间到绝对(wall)的时钟时间。后者通常适用于触发间隔比较大的定时器,因为它可以防止事件时间之间有太大的漂移。
清单4-1显示了一个定时器的例子,它每30秒触发一次,leeway为1秒。因为定时器的时间间隔比较大,所以使用dispatch_walltime
函数来创建调度源。计时器的第一次触发立即发生,随后的事件每30秒到达。MyPeriodicTask
和MyStoreTimer
符号代表自定义函数,编写这些函数来实现定时器行为,并将定时器存储在程序数据结构的某个地方。
下面展示了一个间隔 30s leeaway 1s 的timer。因为间隔较大,dispatch source 使用 dispatch_walltime
来创建的。timer 初次会立即 fire,之后每 30s 到达一次。
清单4-1 创建定时器数据源
1 | dispatch_source_t CreateDispatchTimer(uint64_t interval, |
尽管创建一个定时器调度源是接收基于时间的事件的主要方式,但也有其他选择。如果你想在指定的时间间隔后执行一次block,你可以使用dispatch_after
或dispatch_after_f
函数。这个函数的作用与dispatch_async
函数很相似,只是它允许你指定一个时间值,在这个时间值上将block提交给队列。根据你的需要,时间值可以指定为一个相对的或绝对的时间值。
从描述符中读取数据
要从文件或套接字中读取数据,你必须打开文件或套接字,并创建一个DISPATCH_SOURCE_TYPE_READ
类型的调度源。指定的事件handler应该能够读取和处理文件描述符的内容。在处理文件的情况下,这相当于读取文件数据(或该数据的一个子集),并为程序创建适当的数据结构。对于网络套接字,这涉及到处理新收到的网络数据。
每当读取数据时,你应该始终将描述符配置为使用非阻塞操作。尽管可以使用dispatch_source_get_data
函数来查看有多少数据可供读取,但该函数返回的值在你调用时和你实际读取数据时可能发生变化。如果底层文件被截断或发生网络错误,从描述符中读取的数据会阻塞当前线程,从而使你的事件handler在执行过程中卡死,阻塞调度队列其他任务。对于一个串行队列,这可能会使队列造成死锁,甚至对于一个并发队列,这也会削减可启动的新任务的数量。
清单4-2显示了一个配置调度源以从文件中读取数据的例子。在这个例子中,事件handler将指定文件的全部内容读入一个缓冲区,并调用一个自定义函数来处理这些数据。该函数的调用者将使用返回的调度源,在读取操作完成后取消它。为了确保调度队列在没有数据可读时不会出现不必要的阻塞,本例使用fcntl
函数来配置文件描述符,使其执行非阻塞操作。配置在调度源上的取消handler确保文件描述符在数据被读取后被关闭。
1 | dispatch_source_t ProcessContentsOfFile(const char* filename) |
上面的例子中,自定义的MyProcessFileData
函数决定了什么时候已经读取了足够的文件数据,什么时候取消调度源。默认情况下,为从描述符中读取数据而配置的调度源会在仍有数据需要读取时重复调度其事件handler。如果套接字连接关闭或到达文件的末尾,调度源会自动停止调度事件handler。如果确定不需要一个调度源,可以自己直接取消它。
把数据写入描述符中
向文件或套接字写数据的过程与读数据的过程非常相似。在为写操作配置描述符后,你要创建一个DISPATCH_SOURCE_TYPE_WRITE
类型的调度源。一旦该调度源被创建,系统就会调用你的事件handler,让它有机会开始向文件或套接字写入数据。当你写完数据后,使用dispatch_source_cancel
函数来取消调度源。
无论什么时候写数据,你都应该将文件描述符配置为使用非阻塞操作。尽管你可以使用dispatch_source_get_data
函数来查看有多少空间可供写入,但该函数返回的值只是指导性的,在你调用时和你实际写入数据时可能发生变化。如果发生错误,向一个阻塞的文件描述符写入数据可能会使你的事件handler在执行过程中卡死,并阻塞调度队列其他任务。对于一个串行队列,这可能会使你的队列造成死锁,甚至对于一个并发队列,这也会削减可以启动的新任务的数量。
清单4-3显示了使用调度源向文件写入数据的基本方法。在创建新文件后,该函数将产生的文件描述符传递给其事件handler。被放入文件的数据是由MyGetData
函数提供的,你可以用需要的任何代码来替换它,以生成文件的数据。将数据写入文件后,事件handler取消了调度源,以防止它被再次调用。然后,调度源的所有者将负责释放它。
清单4-3 向文件写入数据
1 | dispatch_source_t WriteDataToFile(const char* filename) |
监控文件系统对象
如果你想监视一个文件系统对象的变化,你可以设置一个DISPATCH_SOURCE_TYPE_VNODE
类型的调度源。你可以使用这种类型的调度源,在文件被删除、写入或重命名时接收通知。你也可以用它在文件的特定类型的元信息(如它的大小和链接数)发生变化时得到通知。
注意:你为调度源指定的文件描述符必须在源本身处理事件时保持打开。
清单4-4显示了一个例子,它监视一个文件名变化,并在它发生变化时执行一些自定义行为。(你可以提供实际的行为来代替例子中调用的 MyUpdateFileName
函数。)因为一个描述符是专门为调度源打开的,所以调度源包含一个关闭描述符的取消handler。因为本例创建的文件描述符与底层文件系统对象相关联,相同的调度源可以用来检测任何数量的文件名变化。
清单4-4 观察文件名变化
1 | dispatch_source_t MonitorNameChangesToFile(const char* filename) |
监控信号
UNIX信号允许从一个程序外对其进行操纵。一个程序可以接收许多不同类型的信号,从不可恢复的错误(如非法指令)到重要信息的通知(如一个子进程退出时)。传统上,程序使用sigaction
函数来配置一个信号处理函数,该函数在信号到达后立即同步处理。如果你只是想得到信号到达的通知,而不是真的想处理信号,你可以使用一个信号调度源来异步处理信号。
信号调度源不能替代使用sigaction
函数配置的同步信号handler。同步信号handler实际上可以捕获一个信号并防止它终止程序。信号调度源允许你只监控信号的到达。此外,你不能使用信号调度源来检索所有类型的信号。具体来说,你不能用它们来监控SIGILL
、SIGBUS
和SIGSEGV
信号。
因为信号调度源是在调度队列上异步执行的,所以它们不受一些与同步信号handler的限制。例如,你可以从信号调度源的事件handler中调用的函数。这种灵活性增加的代价是,在信号到达和调度源的事件handler被调用之间可能会有一些延迟。
清单4-5显示了如何配置一个信号调度源来处理SIGHUP
信号。调度源的事件handler调用了MyProcessSIGHUP
函数,你可以在此实现自己的处理信号逻辑。
清单4-5 配置block监控信号
1 | void InstallSignalHandler() |
如果你正在为一个自定义的框架开发代码,使用信号调度源的一个好处是代码可以独立于任何链接到它的程序来监控信号。信号调度源不会干扰其他调度源或程序可能配置的任何同步信号handler。
监控进程
进程调度源可以让你监控一个特定进程的行为,并作出适当的响应。一个父进程可以使用这种调度源来监视它所创建的任何子进程。例如,父进程可以用它来监视一个子进程的结束。同样地,一个子进程可以用它来监视它的父进程,并在父进程退出时退出。
清单4-6显示了配置一个调度源以监视父进程终止的步骤。当父进程终止时,调度源设置一些内部状态信息,让子进程知道它应该退出。(程序需要实现MySetAppExitFlag
函数来为终止设置一个适当的标志。) 由于调度源自主运行,因此持有自己,它也会在预期程序关闭的情况下取消和释放自己。
清单4-6 监控父进程的终止
1 | void MonitorParentProcess() |
取消调度源
调度源一直处于活动状态,直到你使用dispatch_source_cancel
函数显式取消它们。取消一个调度源会停止新事件的传递,并且不能被撤销。因此,通常取消一个调度源,然后就立即释放它,如下所示:
1 | void RemoveDispatchSource(dispatch_source_t mySource) |
取消一个调度源是一个异步操作。尽管在你调用dispatch_source_cancel
函数后,没有新的事件被处理,但已经被调度源处理的事件还是继续被处理。在处理完任何最终事件后,如果有取消handler,调度源会执行其取消handler。
取消handler是你释放内存或清理代表调度源获取的任何资源的机会。如果调度源使用描述符或mach端口,你必须提供一个取消handler,以便在取消发生时关闭描述符或销毁端口。其他类型的调度源不需要取消handler,但如果你将任何内存或数据与调度源关联,仍应提供。例如,如果你在调度源的上下文指针中存储数据,你应提供取消handler。关于取消handler的更多信息,可参阅Installing a Cancellation Handler。
暂停和恢复调度源
你可以使用dispatch_suspend
和dispatch_resume
方法暂停和恢复调度源事件的传递。这些方法为调度对象增加和减少暂停计数。因此,你必须在每次平衡调用dispatch_suspend
与调用dispatch_resume
。
当暂停一个调度源时,任何在该调度源被暂停时发生的事件都会被收集起来,直到队列恢复。当队列恢复时,不是发送所有的事件,而是在发送前将这些事件合并成一个单一的事件。例如,如果你正在监控一个文件的名称变化,发送的事件将只包括最后的名称变化。以这种方式合并事件,可以防止它们在队列中堆积,并在工作恢复时让你的程序应付不来。
总结
- 调度源用于监听底层(系统、内核)事件,实现处理事件异步回调。既然是用于监听,对应的就该主动取消。
- 在Swift中,根据调度源类型,有对应的协议。但创建都是使用DispatchSource对应的make类工厂方法创建特定类型的调度源。这样接收的调度源返回值就可以调用特定类型协议的具体方法。
- 调度源的通用方法都定义在DispatchSourceProtocol。
- 配置:
setRegistrationHandler
、setEventHandler
、setCancelHandler
- 基本操作:
activate
、cancel
、suspend
、resume
、
- 配置: