Dispatch Sources node
现代系统一般提供异步接口,容许应用向系统提交请求,而后在系统处理请求时应用能够继续处理本身的事情。Grand Central Dispatch正是基于这个基本行为而设计,容许你提交请求,并经过block和dispatch queue报告结果。 编程
dispatch source是基础数据类型,协调特定底层系统事件的处理。Grand Central Dispatch支持如下dispatch source: 服务器
Timer dispatch source:按期产生通知 网络
Signal dispatch source:UNIX信号到达时产生通知 数据结构
Descriptor dispatch source:各类文件和socket操做的通知 并发
数据可读 app
数据可写 异步
文件在文件系统中被删除、移动、重命名 socket
文件元数据信息改变 async
Process dispatch source:进程相关的事件通知
当进程退出时
当进程发起fork或exec等调用
信号被递送到进程
Mach port dispatch source:Mach相关事件的通知
Custom dispatch source:你本身定义并本身触发
Dispatch source替代了异步回调函数,来处理系统相关的事件。当你配置一个dispatch source时,你指定要监测的事件、dispatch queue、以及处理事件的代码(block或函数)。当事件发生时,dispatch source会提交你的block或函数到指定的queue去执行
和手工提交到queue的任务不一样,dispatch source为应用提供连续的事件源。除非你显式地取消,dispatch source会一直保留与dispatch queue的关联。只要相应的事件发生,就会提交关联的代码到dispatch queue去执行。
为了防止事件积压到dispatch queue,dispatch source实现了事件合并机制。若是新事件在上一个事件处理器出列并执行以前到达,dispatch source会将新旧事件的数据合并。根据事件类型的不一样,合并操做可能会替换旧事件,或者更新旧事件的信息。
建立Dispatch Source
建立dispatch source须要同时建立事件源和dispatch source自己。事件源是处理事件所须要的native数据结构,例如基于描述符的dispatch source,你须要打开描述符;基于进程的事件,你须要得到目标程序的进程ID。
而后能够以下建立相应的dispatch source:
使用 dispatch_source_create 函数建立dispatch source
配置dispatch source:
为dispatch source设置一个事件处理器
对于定时器源,使用 dispatch_source_set_timer 函数设置定时器信息
为dispatch source赋予一个取消处理器(可选)调用 dispatch_resume 函数开始处理事件因为dispatch source必须进行额外的配置才能被使用,dispatch_source_create 函数返回的dispatch source将处于挂起状态。此时dispatch source会接收事件,可是不会进行处理。这时候你能够安装事件处理器,并执行额外的配置。
编写和安装一个事件处理器
你须要定义一个事件处理器来处理事件,能够是函数或block对象,并使用 dispatch_source_set_event_handler 或 dispatch_source_set_event_handler_f 安装事件处理器。事件到达时,dispatch source会提交你的事件处理器到指定的dispatch queue,由queue执行事件处理器。
事件处理器的代码负责处理全部到达的事件。若是事件处理器已经在queue中并等待处理已经到达的事件,若是此时又来了一个新事件,dispatch source会合并这两个事件。事件处理器一般只能看到最新事件的信息,不过某些类型的dispatch source也能得到已经发生以及合并的事件信息。
若是事件处理器已经开始执行,一个或多个新事件到达,dispatch source会保留这些事件,直到前面的事件处理器完成执行。而后以新事件再次提交处理器到queue。
函数事件处理器有一个context指针指向dispatch source对象,没有返回值。Block事件处理器没有参数,也没有返回值。
- // Block-based event handler
- void (^dispatch_block_t)(void)
- // Function-based event handler
- void (*dispatch_function_t)(void *)
在事件处理器中,你能够从dispatch source中得到事件的信息,函数处理器能够直接使用参数指针,Block则必须本身捕获到dispatch source指针,通常block定义时会自动捕获到外部定义的全部变量。
- dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
- myDescriptor, 0, myQueue);
- dispatch_source_set_event_handler(source, ^{
- // Get some data from the source variable, which is captured
- // from the parent context.
- size_t estimated = dispatch_source_get_data(source);
- // Continue reading the descriptor...
- });
- dispatch_resume(source);
Block捕获外部变量容许更大的灵活性和动态性。固然,在Block中这些变量默认是只读的,虽然可使用__block来修改捕获的变量,可是你最好不要在事件处理器中这样作。由于Dispatch source异步执行事件处理器,当事件处理器修改原始外部变量时,有可能这些变量已经不存在了。
下面是事件处理器可以得到的事件信息:
函数 | 描述 |
dispatch_source_get_handle | 这个函数返回dispatch source管理的底层系统数据类型。 对于描述符dispatch source,函数返回一个int,表示关联的描述符 对于信号dispatch source,函数返回一个int,表示最新事件的信号数值 对于进程dispatch source,函数返回一个pid_t数据结构,表示被监控的进程 对于Mach port dispatch source,函数返回一个 mach_port_t 数据结构 对于其它dispatch source,函数返回的值未定义 |
dispatch_source_get_data | 这个函数返回事件关联的全部未决数据。 对于从文件中读取数据的描述符dispatch source,这个函数返回能够读取的字节数 对于向文件中写入数据的描述符dispatch source,若是能够写入,则返回正数值 对于监控文件系统活动的描述符dispatch source,函数返回一个常量,表示发生的事件类型,参考 dispatch_source_vnode_flags_t 枚举类型 对于进程dispatch source,函数返回一个常量,表示发生的事件类型,参考 dispatch_source_proc_flags_t 枚举类型 对于Mach port dispatch source,函数返回一个常量,表示发生的事件类型,参考 dispatch_source_machport_flags_t 枚举类型 对于自定义dispatch source,函数返回从现有数据建立的新数据,以及传递给 dispatch_source_merge_data 函数的新数据。 |
dispatch_source_get_mask | 这个函数返回用来建立dispatch source的事件标志 对于进程dispatch source,函数返回dispatch source接收到的事件掩码,参考 dispatch_source_proc_flags_t 枚举类型 对于发送权利的Mach port dispatch source,函数返回指望事件的掩码,参考 dispatch_source_mach_send_flags_t 枚举类型 对于自定义 “或” 的dispatch source,函数返回用来合并数据值的掩码。 |
安装一个取消处理器
取消处理器在dispatch soruce释放以前执行清理工做。多数类型的dispatch source不须要取消处理器,除非你对dispatch source有自定义行为须要在释放时执行。可是使用描述符或Mach port的dispatch source必须设置取消处理器,用来关闭描述符或释放Mach port。不然可能致使微妙的bug,这些结构体会被系统其它部分或你的应用在不经意间重用。
你能够在任什么时候候安装取消处理器,但一般咱们在建立dispatch source时就会安装取消处理器。使用 dispatch_source_set_cancel_handler 或 dispatch_source_set_cancel_handler_f 函数来设置取消处理器。
下面取消处理器关闭描述符:
- dispatch_source_set_cancel_handler(mySource, ^{
- close(fd); // Close a file descriptor opened earlier.
- });
修改目标Queue
在建立dispatch source时能够指定一个queue,用来执行事件处理器和取消处理器。不过你也可使用 dispatch_set_target_queue 函数在任什么时候候修改目标queue。修改queue能够改变执行dispatch source事件的优先级。
修改dispatch source的目标queue是异步操做,dispatch source会尽量快地完成这个修改。若是事件处理器已经进入queue并等待处理,它会继续在原来的Queue中执行。随后到达的全部事件的处理器都会在后面修改的queue中执行。
关联自定义数据到dispatch source
和Grand Central Dispatch的其它类型同样,你可使用 dispatch_set_context 函数关联自定义数据到dispatch source。使用context指针存储事件处理器须要的任何数据。若是你在context指针中存储了数据,你就应该安装一个取消处理器,在dispatch source再也不须要时释放这些context自定义数据。
若是你使用block实现事件处理器,你也能够捕获本地变量,并在Block中使用。虽然这样也能够代替context指针,可是你应该明智地使用Block捕获变量。由于dispatch source长时间存在于应用中,Block捕获指针变量时必须很是当心,由于指针指向的数据可能会被释放,所以须要复制数据或retain。无论使用哪一种方法,你都应该提供一个取消处理器,在最后释放这些数据。
Dispatch Source的内存管理
Dispatch Source也是引用计数的数据类型,初始计数为1,可使用 dispatch_retain 和 dispatch_release 函数来增长和减小引用计数。引用计数到达0时,系统自动释放dispatch source数据结构。
dispatch source的全部权能够由dispatch source内部或外部进行管理。外部全部权时,另外一个对象拥有dispatch source,并负责在不须要时释放它。内部全部权时,dispatch source本身拥有本身,并负责在适当的时候释放本身。虽然外部全部权很经常使用,当你但愿建立自主dispatch source,并让它本身管理本身的行为时,可使用内部全部权。例如dispatch source应用单一全局事件时,可让它本身处理该事件,并当即退出。
Dispatch Source示例
建立一个定时器
定时器dispatch source定时产生事件,能够用来发起定时执行的任务,如游戏或其它图形应用,可使用定时器来更新屏幕或动画。你也能够设置定时器,并在固定间隔事件中检查服务器的新信息。
全部定时器dispatch source都是间隔定时器,一旦建立,会按你指定的间隔按期递送事件。你须要为定时器dispatch source指定一个指望的定时器事件精度,也就是leeway值,让系统可以灵活地管理电源并唤醒内核。例如系统可使用leeway值来提早或延迟触发定时器,使其更好地与其它系统事件结合。建立本身的定时器时,你应该尽可能指定一个leeway值。
就算你指定leeway值为0,也不要指望定时器可以按照精确的纳秒来触发事件。系统会尽量地知足你的需求,可是没法保证彻底精确的触发时间。
当计算机睡眠时,定时器dispatch source会被挂起,稍后系统唤醒时,定时器dispatch source也会自动唤醒。根据你提供的配置,暂停定时器可能会影响定时器下一次的触发。若是定时器dispatch source使用 dispatch_time 函数或 DISPATCH_TIME_NOW 常量设置,定时器dispatch source会使用系统默认时钟来肯定什么时候触发,可是默认时钟在计算机睡眠时不会继续。
若是你使用 dispatch_walltime 函数来设置定时器dispatch source,则定时器会根据挂钟时间来跟踪,这种定时器比较适合触发间隔相对比较大的场合,能够防止定时器触发间隔出现太大的偏差。
下面是定时器dispatch source的一个例子,每30秒触发一次,leeway值为1,由于间隔相对较大,使用 dispatch_walltime 来建立定时器。定时器会当即触发第一次,随后每30秒触发一次。 MyPeriodicTask 和 MyStoreTimer 是自定义函数,用于实现定时器的行为,并存储定时器到应用的数据结构。
- dispatch_source_t CreateDispatchTimer(uint64_t interval,
- uint64_t leeway,
- dispatch_queue_t queue,
- dispatch_block_t block)
- {
- dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
- 0, 0, queue);
- if (timer)
- {
- dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
- dispatch_source_set_event_handler(timer, block);
- dispatch_resume(timer);
- }
- return timer;
- }
- void MyCreateTimer()
- {
- dispatch_source_t aTimer = CreateDispatchTimer(30ull * NSEC_PER_SEC,
- 1ull * NSEC_PER_SEC,
- dispatch_get_main_queue(),
- ^{ MyPeriodicTask(); });
- // Store it somewhere for later use.
- if (aTimer)
- {
- MyStoreTimer(aTimer);
- }
- }
虽然定时器dispatch source是接收时间事件的主要方法,你还可使用其它选择。若是想在指定时间间隔后执行一个block,可使用 dispatch_after 或 dispatch_after_f 函数。这两个函数很是相似于dispatch_async,可是只容许你指定一个时间值,时间一到就自动提交block到queue中执行,时间值能够指定为相对或绝对时间。
从描述符中读取数据
要从文件或socket中读取数据,须要打开文件或socket,并建立一个 DISPATCH_SOURCE_TYPE_READ 类型的dispatch source。你指定的事件处理器必须可以读取和处理描述符中的内容。对于文件,须要读取文件数据,并为应用建立适当的数据结构;对于网络socket,须要处理最新接收到的网络数据。
读取数据时,你老是应该配置描述符使用非阻塞操做,虽然你可使用 dispatch_source_get_data 函数查看当前有多少数据可读,但在你调用它和实际读取数据之间,可用的数据数量可能会发生变化。若是底层文件被截断,或发生网络错误,从描述符中读取会阻塞当前线程,中止在事件处理器中间并阻止dispatch queue去执行其它任务。对于串行queue,这样还可能会死锁,即便是并发queue,也会减小queue可以执行的任务数量。
下面例子配置dispatch source从文件中读取数据,事件处理器读取指定文件的所有内容到缓冲区,并调用一个自定义函数来处理这些数据。调用方可使用返回的dispatch source在读取操做完成以后,来取消这个事件。为了确保dispatch queue不会阻塞,这里使用了fcntl函数,配置文件描述符执行非阻塞操做。dispatch source安装了取消处理器,确保最后关闭了文件描述符。
- dispatch_source_t ProcessContentsOfFile(const char* filename)
- {
- // Prepare the file for reading.
- int fd = open(filename, O_RDONLY);
- if (fd == -1)
- return NULL;
- fcntl(fd, F_SETFL, O_NONBLOCK); // Avoid blocking the read operation
- 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;
- }
- // Install the event handler
- dispatch_source_set_event_handler(readSource, ^{
- size_t estimated = dispatch_source_get_data(readSource) + 1;
- // Read the data into a text buffer.
- char* buffer = (char*)malloc(estimated);
- if (buffer)
- {
- ssize_t actual = read(fd, buffer, (estimated));
- Boolean done = MyProcessFileData(buffer, actual); // Process the data.
- // Release the buffer when done.
- free(buffer);
- // If there is no more data, cancel the source.
- if (done)
- dispatch_source_cancel(readSource);
- }
- });
- // Install the cancellation handler
- dispatch_source_set_cancel_handler(readSource, ^{close(fd);});
- // Start reading the file.
- dispatch_resume(readSource);
- return readSource;
- }
在这个例子中,自定义的 MyProcessFileData 函数肯定读取到足够的数据,返回YES告诉dispatch source读取已经完成,能够取消任务。一般读取描述符的dispatch source在还有数据可读时,会重复调度事件处理器。若是socket链接关闭或到达文件末尾,dispatch source自动中止调度事件处理器。若是你本身肯定再也不须要dispatch source,也能够手动取消它。
向描述符写入数据
向文件或socket写入数据很是相似于读取数据,配置描述符为写入操做后,建立一个 DISPATCH_SOURCE_TYPE_WRITE 类型的dispatch source,建立好以后,系统会调用事件处理器,让它开始向文件或socket写入数据。当你完成写入后,使用 dispatch_source_cancel 函数取消dispatch source。
写入数据也应该配置文件描述符使用非阻塞操做,虽然 dispatch_source_get_data 函数能够查看当前有多少可用写入空间,但这个值只是建议性的,并且在你执行写入操做时可能会发生变化。若是发生错误,写入数据到阻塞描述符,也会使事件处理器中止在执行中途,并阻止dispatch queue执行其它任务。串行queue会产生死锁,并发queue则会减小可以执行的任务数量。
下面是使用dispatch source写入数据到文件的例子,建立文件后,函数传递文件描述符到事件处理器。MyGetData函数负责提供要写入的数据,在数据写入到文件以后,事件处理器取消dispatch source,阻止再次调用。此时dispatch source的拥有者需负责释放dispatch source。
- dispatch_source_t WriteDataToFile(const char* filename)
- {
- int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC,
- (S_IRUSR | S_IWUSR | S_ISUID | S_ISGID));
- if (fd == -1)
- return NULL;
- fcntl(fd, F_SETFL); // Block during the write.
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_source_t writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE,
- fd, 0, queue);
- if (!writeSource)
- {
- close(fd);
- return NULL;
- }
- dispatch_source_set_event_handler(writeSource, ^{
- size_t bufferSize = MyGetDataSize();
- void* buffer = malloc(bufferSize);
- size_t actual = MyGetData(buffer, bufferSize);
- write(fd, buffer, actual);
- free(buffer);
- // Cancel and release the dispatch source when done.
- dispatch_source_cancel(writeSource);
- });
- dispatch_source_set_cancel_handler(writeSource, ^{close(fd);});
- dispatch_resume(writeSource);
- return (writeSource);
- }
监控文件系统对象
若是须要监控文件系统对象的变化,能够设置一个 DISPATCH_SOURCE_TYPE_VNODE 类型的dispatch source,你能够从这个dispatch source中接收文件删除、写入、重命名等通知。你还能够获得文件的特定元数据信息变化通知。
在dispatch source正在处理事件时,dispatch source中指定的文件描述符必须保持打开状态。
下面例子监控一个文件的文件名变化,并在文件名变化时执行一些操做(自定义的 MyUpdateFileName 函数)。因为文件描述符专门为dispatch source打开,dispatch source安装了取消处理器来关闭文件描述符。这个例子中的文件描述符关联到底层的文件系统对象,所以同一个dispatch source能够用来检测屡次文件名变化。
- dispatch_source_t MonitorNameChangesToFile(const char* filename)
- {
- int fd = open(filename, O_EVTONLY);
- if (fd == -1)
- return NULL;
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE,
- fd, DISPATCH_VNODE_RENAME, queue);
- if (source)
- {
- // Copy the filename for later use.
- int length = strlen(filename);
- char* newString = (char*)malloc(length + 1);
- newString = strcpy(newString, filename);
- dispatch_set_context(source, newString);
- // Install the event handler to process the name change
- dispatch_source_set_event_handler(source, ^{
- const char* oldFilename = (char*)dispatch_get_context(source);
- MyUpdateFileName(oldFilename, fd);
- });
- // Install a cancellation handler to free the descriptor
- // and the stored string.
- dispatch_source_set_cancel_handler(source, ^{
- char* fileStr = (char*)dispatch_get_context(source);
- free(fileStr);
- close(fd);
- });
- // Start processing events.
- dispatch_resume(source);
- }
- else
- close(fd);
- return source;
- }
监测信号
应用能够接收许多不一样类型的信号,如不可恢复的错误(非法指令)、或重要信息的通知(如子进程退出)。传统编程中,应用使用 sigaction 函数安装信号处理器函数,信号到达时同步处理信号。若是你只是想信号到达时获得通知,并不想实际地处理该信号,可使用信号dispatch source来异步处理信号。
信号dispatch source不能替代 sigaction 函数提供的同步信号处理机制。同步信号处理器能够捕获一个信号,并阻止它停止应用。而信号dispatch source只容许你监测信号的到达。此外,你不能使用信号dispatch source获取全部类型的信号,如SIGILL, SIGBUS, SIGSEGV信号。
因为信号dispatch source在dispatch queue中异步执行,它没有同步信号处理器的一些限制。例如信号dispatch source的事件处理器能够调用任何函数。灵活性增大的代价是,信号到达和dispatch source事件处理器被调用的延迟可能会增大。
下面例子配置信号dispatch source来处理SIGHUP信号,事件处理器调用 MyProcessSIGHUP 函数,用来处理信号。
- void InstallSignalHandler()
- {
- // Make sure the signal does not terminate the application.
- signal(SIGHUP, SIG_IGN);
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGHUP, 0, queue);
- if (source)
- {
- dispatch_source_set_event_handler(source, ^{
- MyProcessSIGHUP();
- });
- // Start processing signals
- dispatch_resume(source);
- }
- }
监控进程
进程dispatch source能够监控特定进程的行为,并适当地响应。父进程可使用dispatch source来监控本身建立的全部子进程,例如监控子进程的死亡;相似地,子进程也可使用dispatch source来监控父进程,例如在父进程退出时本身也退出。
下面例子安装了一个进程dispatch source,监控父进程的终止。当父进程退出时,dispatch source设置一些内部状态信息,告知子进程本身应该退出。MySetAppExitFlag 函数应该设置一个适当的标志,容许子进程终止。因为dispatch source自主运行,所以本身拥有本身,在程序关闭时会取消并释放本身。
- void MonitorParentProcess()
- {
- pid_t parentPID = getppid();
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
- parentPID, DISPATCH_PROC_EXIT, queue);
- if (source)
- {
- dispatch_source_set_event_handler(source, ^{
- MySetAppExitFlag();
- dispatch_source_cancel(source);
- dispatch_release(source);
- });
- dispatch_resume(source);
- }
- }
取消一个Dispatch Source
除非你显式地调用 dispatch_source_cancel 函数,dispatch source将一直保持活动,取消一个dispatch source会中止递送新事件,而且不能撤销。所以你一般在取消dispatch source后当即释放它:
- void RemoveDispatchSource(dispatch_source_t mySource)
- {
- dispatch_source_cancel(mySource);
- dispatch_release(mySource);
- }
取消一个dispatch source是异步操做,调用 dispatch_source_cancel 以后,不会再有新的事件被处理,可是正在被dispatch source处理的事件会继续被处理完成。在处理完最后的事件以后,dispatch source会执行本身的取消处理器。
取消处理器是你最后的执行机会,在那里执行内存或资源的释放工做。例如描述符或mach port类型的dispatch source,必须提供取消处理器,用来关闭描述符或mach port
挂起和继续Dispatch Source
你可使用 dispatch_suspend 和 dispatch_resume 临时地挂起和继续dispatch source的事件递送。这两个函数分别增长和减小dispatch 对象的挂起计数。所以,你必须每次 dispatch_suspend 调用以后,都须要相应的 dispatch_resume 才能继续事件递送。
挂起一个dispatch source期间,发生的任何事件都会被累积,直到dispatch source继续。可是不会递送全部事件,而是先合并到单一事件,而后再一次递送。例如你监控一个文件的文件名变化,就只会递送最后一次的变化事件。