Dispatch Sources

本文深入探讨了GCD中的Dispatch Sources,一种用于监听低级别系统事件的机制。介绍了不同类型的Dispatch Sources及其应用场景,包括定时器、信号、描述符等,并提供了具体的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在你需要与底层系统交互时,就要做好该交互任务会持续一段时间的准备了。无论是内核抑或是其他系统层级的背景改变都肯定会费时,与把任务放进自己进程中相比较。于是,许多系统库都提供了异步接口来允许个人代码提供系统请求然后在请求在被处理的同时可以做其他的任务。GCD通过提供blocks 和 dispatch queues来使个人可以提交请求并取得结果报告。

关于Dispatch Sources

Dispatch Sources是协调特定低级别系统事件处理的基本数据类型。GCD支持以下类型的Dispatch Sources:

  • Timer dispatch sources 生成定时通知。
  • Signal dispatch sources 当UNIX信号(分时处理器信号)到达时通知你
  • Descriptor sources 会根据多种基于文件信息或socket操作通知你,例如

    • 当数据可读时
    • 当数据可写时
    • 当文件在文件系统中被删除/移动/重命名时
    • 当文件元信息更改时
  • Process dispatch sources 通知进程相关事件,例如

    • 当一个进程存在时
    • 当一个进程发出fork或exec类型的调用时
    • 当一个信号传递到进程时
  • Mach port dispatch sources 通知Mach相关事件

  • Custom dispatch sources 是自定义自触发

Dispatch sources 替代异步回调方法,常用于处理系统相关事件。在配置一个 dispatch source时,你需要指定你想要监听的事件s和相应的dispatch queue以及处理该事件s的代码,可以利用block或者方法函数去指定你的代码。当你感兴趣的事件发生了,刚才配置好的dispatch source 就会提交你的block或函数到指定的dispatch queue 去执行。

与你手动提交到队列的任务不同,dispatch source给你的应用提供连续的事件源。一个dispatch source会保持与它的dispatch queue的连接直到你明确取消它。在连接期间,当相应的事件发生时,dispatch source会提交相关任务代码给dispatch queue。这些事件中时间事件会隔一段时间发生但是大多数类型事件会在特殊条件下零星的发生。基于此,dispatch sources会保持它们相关的dispatch queue来防止这些事件在即将发生或消失时它被提前释放了。

为防止事件在dispatch queue中积压,dispatch sources实现了事件联合机制。如果当一个新的事件发生时之前的事件还未处理完,那个dispatch sources便会将新事件的数据与老事件的数据联合起来。联合数据的方式根据事件类型会有直接替代老数据或者更新数据。例如,一个基于信号的dispatch sources会提供最新的信号以及最近执行的事件处理后收到的信号总数。

Creating Dispatch Sources

创建一个dispatch source需要创建事件源以及dispatch source本身。事件源就是处理事件所需的无论何种格式的本地数据结构体。比如,描述性 类型dispatch source 你需要打开描述文件,进程类型源你需要获取目标项目的进程ID。当你有了你的事件源,你就可以创建相应的dispatch source 了,如下所示:

  • 1.利用dispatch_source_create 函数来创建dispatch source
  • 2.配置dispatch source
    • 设置相应的事件处理
    • 针对时间源,利用dispatch_source_set_timer 函数设置时间信息
  • 3.选择性的设置取消处理
  • 4.调用dispatch_resume 函数开始处理事件

因为dispatch source 需要一些具体配置才可以使用,所以 dispatch_source_create 函数返回的 dispatch sources 是处于挂起状态的。在挂起状态下,一个dispatch source 可以接受事件但是不可以处理事件。这样就给了你安装事件处理以及执行具体配置去处理事件留下了时间。

Writing and Installing an Event Handler

想要处理由一个dispatch source 生成的事件,首先必须要第一个你一个处理这些事件的事件处理。一个事件处理就是利用dispatch_source_set_event_handler 或则 dispatch_source_set_event_handler_f方法安装到你的dispatch source上的一个函数或者block对象。当一个事件到达时,该dispatch source 便会提交你的事件处理到指定的队列中去处理。

事件处理的内容应是可以处理任何到达的事件的。如果当一个新的事件到达时,你的事件处理已经在队列中等待处理一个事件那么该dispatch source 便会联合这两个事件。一个事件处理通常只会看最近的事件信息,但是根据dispatch source 的类型也可能会获取其他发生的并联合的事件信息。如果一个或者多个事件到达时事件处理已经开始工作了,则dispatch source 会保持这些事件直至当前事件处理结束工作。到时,它会提交这些事件到事件处理。

基于函数的事件处理包含一个context指针,含有dispatch source 对象不返回值。基于block的事件处理则没有参数没有返回值。

   // Block-based event handler
   void (^dispatch_block_t)(void)

   // Function-based event handler
   void (*dispatch_function_t)(void *)

在你的事件处理内部,你可以通过给出事件的dispatch source 本身获取该事件的信息。虽然基于函数的事件处理被传递一个指针给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);

虽然在某些特殊情况下可以修改获取的变量,但是不建议在与一个dispatch source 相关的事件处理中做上诉尝试。dispatch sources 经常异步执行它们的事件处理,所以你获取的变量的定义范围很有可能会随着你的事件处理的执行而消失。

方法描述
dispatch_source_get_handle这个方法返回该dispatch source管理的底层系统数据类型
dispatch_source_get_data这个方法返回与事件相关的数
dispatch_source_get_mask这个方法返回用来创建dispatch source的事件flags
Installing a Cancellation Handler

取消处理操作用来清理一个dispatch source ,在它释放之前。对于大部分类型的dispatch sources 取消处理操作都是可选的,除非在你有一些与dispatch source 绑定的自定义的操作需要更新。针对 descriptor 或则 Mach port 类型必须提供取消处理操作去关闭descriptor或则释放 Mach port 。否则会出现一些bug。

你可以在任何时刻安装取消处理操作,但是我们更偏向于在创建dispatch source 时安装。你可以选择dispatch_source_set_cancel_handler 或dispatch_source_set_cancel_handler_f 方法来安装取消处理操作,这取决于你是想用函数抑或是block。eg:

dispatch_source_set_cancel_handler(mySource, ^{
close(fd); // Close a file descriptor opened earlier.
});
Changing the Target Queue

虽然在创建dispatch source 的时候已经指定了处理事件以及取消处理操作的queue ,但是你仍然可在任意时刻使用dispatch_set_target_queue 方法来更改目标queue 。你可在想更改处理dispatch source 事件的queue 的优先级上使用这种情形。更改dispatch source 的queue 是异步操作,而dispatch source 也会努力将这个操作做到最快。 如果在更改dispatch source 的queue 时已有事件在原有队列中并且等待被处理,那么它会在前一个queue 中执行。如果事件到达的时候刚好时你做出了更改,那么这些事件会在其余的queue中被处理。

Associating Custom Data with a Dispatch Source

和GCD中的其他数据类型类时,你可以使用 dispatch_set_context 函数去联系一个dispatch source 的自定义数据。你可以使用context 指针去存储任意你的处理操作需要的数据。如果你已经这样做了,你也应该安装一个取消处理操作去释放那些数据当dispatch source 不再被需要。

如果你使用blocks实现你的事件处理操作,你也可以获取本地变量并利用你的基于块代码来处理它们。虽然这样可以弱化在 dispatch source 的context 指针中存储数据的必要性,但建议你仍然需要审慎的这样做。因为dispatch source 会在你的应用中长时间的存在,所以你需要在去获取带有指针的变量时仔细些。你应该copy或retain指针指向的数据防止这些数据会被释放。当然你也需要为这些数据提供取消处理操作。

Memory Management for Dispatch Sources

和其他dispatch 对象一样,dispatch sources 也是引用计数类型。一个dispatch sources 在初始化的时候引用计数为1,可以使用 dispatch_retain 和 dispatch_release 方法实现被持有或者释放。当一个队列的引用计数为0时,系统会自动释放dispatch source 结构数据。

根据使用方式的不同,dispatch source 自身的拥有权管理可分为内部管理和外部管理两种方式。外部管理的话,拥有占有权的其他对象或者代码的一部分负责在释放它。内部管理的话,该dispatch source 自身拥有自身,而且会在恰当的时候释放自身。虽然外部拥有更为常见,但是你可以在你需要创建一个自主的dispatch source ,不想让它涉及到与其他交互中只是管理自己的代码时使用内部管理。例如,如果需要一个dispatch source 设计成处理一个单独的全局事件,你也许用它来处理事件然后立即退出。

例子

Creating a Timer

Timer dispatch sources 生成有规律的、基于时间的事件。你可以利用times 去指定需要规律化处理的任务。例如游戏或者图形密集型的应用可能需要times 去初始化屏幕或更新动画。你也可以创建一个timer 然后利用结果事件去检查新的信息或一个经常更新的服务。

当创建一个timer dispatch source 时,需要明白确定的一个值就是时间偏差的值用于时间事件的需求精确度上。偏差值给了系统弹性时间用来管理点用和唤醒核心。例如,系统会利用偏差值来提前或延后启动时间然后更好的与其他系统事件保持同步。

  • 需要注意的是,即使你给偏差值赋值为0,你也不要期望它会在精确的十亿分之一秒时启动时间如你期望的。系统会尽可能的满足你的需求,但是不会保证精确的启动时间。

当一个电脑进入睡眠模式,所有的timer dispatch sources 都将暂停。当电脑恢复启动时那些timer dispatch sources 也会自动启动。依据timer 的配置,这种自然的暂停也许会影响你的timer 的下次触发。如果你使用dispatch_time 函数或则 DISPATCH_TIME_NOW 常量来创建你的timer dispatch source ,则timer dispatch source使用系统默认时钟来决定何时触发。但是,默认时钟在电脑睡眠时不会提前。与之相比较的,如果你使用dispatch_walltime 方法来创建你的timer dispatch source ,则timer dispatch source会将它的触发时间跟随挂钟时间。后者的选项更符合时间间隔相对大些的情况,因为它阻止事件事件间有太大的偏差。eg:

  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);
}
}

虽然使用 timer dispatch source 是主要的方式来创建基于时间的事件,也有一些可选选项可供使用。如果你想每隔一段特定的时间执行一个block,可以使用dispatch_after 或则 dispatch_after_f 方法。这个方法更像一个 dispatch_async 方法只不过允许你定一个时间值去决定何时提交block到一个queue中。这个时间值可以根据你的需求被定为相对的或者绝对的。

Reading Data from a Descriptor

从一个文件或者socket中读取数据,必须先打开文件或者socket 去创建DISPATCH_SOURCE_TYPE_READ 类型的dispatch source 。你定义的事件处理操作必须有能力读取和处理文件描述的内容。针对文件类型,这就相当于读取文件数据抑或是部分数据然后为你的应用创建合适的结构数据。对于一个网络socket,这就包含了处理最新接收的网络数据。

无论何时读取数据,你都应该配置你的descriptor 去使用non-blocking 操作。虽然你可以使用 dispatch_source_get_data 方法来看可读的数据的数量,但是返回的数据会在你调用这个方法时间和你实际读取的时间中变化。如果底层文件损坏或则发生网络错误,读取descriptor 会阻塞当前线程使你的事件处理操作处于中期执行境地,而且会阻止队列处理其他任务。对于一个串行队列这样会锁死你的queue,甚至于对于并行队列这也会减少可以开始新任务的数量。eg :

     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;
}
Writing Data to a Descriptor

往文件或者socket中写入数据的过程与读取过程极其相似。在配置了写入操作的descriptor 后,你要创建一个DISPATCH_SOURCE_TYPE_WRITE 类型的dispatch source 。一旦dispatch source 创建,系统会调用你的事件处理操作去让它有机会开始往文件或socket 中写入数据。当你结束写入数据操作时,需要使用dispatch_source_cancel 方法来取消该dispatch source 。

无论何时写入数据,你都应该用non-blocking 操作来配置你的文件描述。虽然你可以使用dispatch_source_get_data 方法去看可供写入的空间,但是该方法返回的值仅仅是咨询性质的,很有可能在你调用该方法以及你实际去写入数据的时候该值会有所变动。如果发生了错误,向一个blocking 文件描述写数据会阻塞当前线程使你的事件处理操作处于中期执行境地,而且会阻止队列处理其他任务。对于一个串行队列这样会锁死你的queue,甚至于对于并行队列这也会减少可以开始新任务的数量。eg:

      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);
}
Monitoring a File-System Object

如果你想监测一个系统文件或其改变,你可以创建一个DISPATCH_SOURCE_TYPE_VNODE 类型的dispatch source 。当一个文件被删除、写入或者是重命名时你可以使用这种类型的dispatch source 来接收相关通知。你也可以当特殊类型的文件头信息改变时用它来提醒自己(例如文件大小或者关联数量)。

  • 你指定的文件描述应在dispatch source处理事件期间保持打开状态。

eg:

        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;
 }
Monitoring Signals

UNIX 信号允许超出其领域的应用操作。一个应用可以接受很多类型的信号,从不可恢复的错误(如非法指令)到关键信息的通知(如子线程的生成)。传统的,应用可以使用sigaction 方法去安装一个信号处理函数,信号处理函数可以在信号一到达的时刻便处理它们。如果你仅仅是想获得信号到达通知并不准备处理该信号,你可以使用一个信号dispatch source 去异步处理那些信号。

Signal dispatch sources 并不是你使用sigaction 方法并安装异步信号处理操作的替代品。Signal dispatch sources 可以实际的抓取一个信号并阻止该信号关闭你的应用。Signal dispatch sources 允许你仅仅监听信号的到达。另外,你不能使用signal dispatch sources 去监听所有类型信号。尤其是,你不能通过它们去监听 SIGILL, SIGBUS, 和 SIGSEGV 信号。

由于signal dispatch sources 在一个dispatch queue 上是被异步处理的,所以他们不会不会像异步信号处理操作那样受到一些限制。例如,你调用你的 signal dispatch source 的事件处理操作方法是没有限制的。增大的灵活性的权衡实质是可能有一些增加的潜在因素在信号到达以及你的dispatch source 的事件处理操作被调用时间之内。eg:

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);
 }
} 
Monitoring a Process

process dispatch source 可以让你监听指定的进程的行为并给出恰当的反应。一个父进程可以通过使用这种类型的dispatch source 去监听它创建的子进程。例如,父进程可以通过它来观察子进程的消亡。类似的,一个子进程也可以通过它来监测它的父进程,随着父进程的退出而退出。

eg:

     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);
  }
 }

PS:由于笔者水平有限,文中肯定有些不尽人意的地方和一些错误,希望各位朋友加以斧正,可查看官方文档来获取更多的信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值