当我们与底层系统进行交互时,必须为该任务做好准备,以便只花费少量时间。调用内核或其他系统层涉及上下文的变化,与在我们自己的进程中发生的调用相比,这种变化相当昂贵。因此,许多系统库提供异步接口,以允许我们的代码向系统提交请求,并在处理该请求时继续执行其他工作。Grand Central Dispatch通过允许我们提交请求并使用block和调度队列将结果报告回我们的代码来构建此一般行为。
关于调度源
调度源是协调特定底层系统事件处理的基本数据类型。Grand Central Dispatch支持以下类型的调度源:
- 定时器调度源定期生成通知。
- 信号调度源在UNIX信号到达时通知我们。
- 描述符源通知我们各种基于文件和基于套接字的操作,例如:
- 当数据可供读取时。
- 当可以写入数据时。
- 在文件系统中删除,移动或重命名文件时。
- 文件元信息发生变化时。
- 进程调度源通知我们与进程相关的事件,例如:
- 当一个进程退出时。
- 当进程发出一个fork或者exec调用类型时。
- 当一个信号被传递给进程时。
- Mach port调度源通知我们Mach相关的事件。
- 自定义调度源由我们自己定义并触发。
调度源取代了通常用于处理系统相关事件的异步回调函数。配置调度源时,可以指定要监听的事件以及用于处理这些事件的调度队列和代码。可以使用block对象或函数来指定我们的代码。当监听到事件触发时,调度源将block或函数提交给指定的调度队列执行。
与手动提交到队列的任务不同,调度源为应用程序提供连续的事件源。调度源会保留其附加的调度队列,直到我们明确取消调度。附加调度队列后,只要相应的事件发生,调度源就会将相关的任务代码提交给调度队列。某些事件(如定时器事件)会定期发生,但大多数情况下只会在特定条件出现时偶发出现。因此,调度源保留其关联的调度队列,以防止可能仍在等待中的事件过早释放。
为防止事件在调度队列中积压,调度源实现了一个事件合并策略。如果新事件在旧事件的事件处理程序已经被取出队列并且实际执行之前到达,那么调度源会将新事件数据中的数据与旧事件中的数据合并。根据事件的类型,合并可能会取代旧事件或更新其保存的信息。例如,基于信号的调度源仅提供关于最新信号的信息,但也报告自从最后一次调用事件处理程序以来已传递了多少信号。
创建调度源
创建调度源涉及创建事件源和调度源本身。事件源是处理事件所需的本地数据结构。例如,对于基于描述符的调度源,需要打开描述符和一个需要用来获取目标程序的进程ID的基于进程的源。我们可以为我们的事件源创建相应的调度源,如下所示:
- 使用
dispatch_source_create
函数创建调度源。 - 配置调度源:
- 为调度源分配一个事件处理程序。
- 对于定时器源,使用
dispatch_source_set_timer
函数设置定时器信息。
- (可选)分配一个取消处理程序给调度源。
- 调用
dispatch_resume
开始处理事件。
由于调度源在能够使用之前还需要一些额外的配置,dispatch_source_create
函数会返回处于暂停状态的调度源。在暂停期间,调度源接收事件但是不处理它们。这样就给了我们时间去设置一个事件处理程序并执行处理实际事件所需的任何其他配置。
以下部分展示如何配置调度源的各个方面。有关如何配置具体调度源的类型的详细示例,请参看调度源示例。
安装取消事件处理程序
取消事件处理程序用于在调度源被释放前执行一些清理操作。对于大多数类型的调度源,取消事件处理程序是可选的,并且只有在我们需要更新与调度源相关的一些自定义行为时才是必需的。但是,对于使用描述符或Mach端口的调度源,必须提供一个取消事件处理程序来关闭描述符或释放Mach端口。如果不这样做,可能会导致代码中或系统的其他部分无意中重复使用这些结构,从而导致代码中出现细微的错误。
可以在任何时候安装取消事件处理程序,但通常在创建调度源时就这样做。可以使用dispatch_source_set_cancel_handler
或dispatch_source_set_cancel_handler_f
函数来安装取消事件处理程序,具体取决于是要在实现中使用block对象还是函数。以下示例显示了一个简单的用于关闭被调度源打开的描述符的取消事件处理程序。
dispatch_source_set_cancel_handler(mySource, ^{
close(fd); // Close a file descriptor opened earlier.
});
更改目标队列
尽管在创建调度源时会指定运行事件和取消事件处理程序的队列,但可以随时使用dispatch_set_target_queue
函数来更改该队列。当我们需要更改处理调度源事件的优先级的时候,可以这样做。
更改调度源的队列是异步执行的操作,调度源会尽可能快地进行更改。如果事件处理程序已经在排队等待处理,它将在前一个队列上执行。但是,在进行更改时到达的其他事件可能会在任一队列中处理。
将自定义数据与调度源相关联
像Grand Central Dispatch中的许多其他数据类型一样,可以使用dispatch_set_context
函数将自定义数据与调度源相关联。可以使用上下文指针来存储事件处理程序处理事件所需的任何数据。如果有任何自定义数据存储在上下文指针中,则还应该安装取消事件处理程序以在不再需要调度源时释放该数据。
如果使用block来实现事件处理程序,则还可以捕获局部变量并在基于block的代码中使用它们。尽管这可能会缓解将数据存储在调度源的上下文指针中的需要,但应该始终谨慎地使用此功能。由于调度源可能在应用程序中长期存在,因此在捕获包含指针的变量时应该小心。如果指针指向的数据可以随时释放,则应该复制数据或保留数据以防止发生这种情况。无论哪种情况,都需要提供一个取消事件处理程序以便稍后释放数据。
调度源的内存管理
像其他调度对象一样,调度源是被引用计数的数据类型。调度源的初始引用计数为1,可以使用dispatch_retain
和dispatch_release
函数来引用和释放。当队列的引用计数为零时,系统会自动释放调度源数据结构。
由于它们的使用方式,调度源的所有权可以在调度源本身内部或外部进行管理。通过外部所有权,另一个对象或代码片段将获取调度源的所有权,并负责在不再需要时释放它。在内部拥有的情况下,调度源持有自己本身,并负责在适当的时候自行释放。尽管外部所有权非常普遍,但如果希望创建自主调度源并让它在没有任何进一步交互的情况下管理代码的的某些行为,我们可能会使用内部所有权。例如,如果调度源被设计为响应单个全局事件,则可能需要它处理该事件,然后立即退出。
调度源示例
创建一个定时器
定时器调度源基于时间间隔定期生成事件,可以使用定时器启动需要定期执行的任务。例如,游戏和其他图形密集型应用程序可能会使用定时器来启动屏幕或者动画更新,也可以设置一个定时器并设置结果事件检查经常更新的服务器上的新信息。
所有定时器调度源都是间隔定时器--即一旦创建,它们会在我们指定的时间间隔传递定期事件。当创建一个定时器调度源时,误差值是必须指定的值之一,它能够使系统了解定时器事件所需的精度。误差值为系统管理功耗和唤醒内核提供了一定的灵活性。例如,系统可能会使用误差值来提前或者延迟触发时间,并将其与其他系统事件更好地对齐。因此,我们应该尽可能为定时器指定一个误差值。
注意:即使我们指定误差值为0,也绝对不要期望一个定时器在要求的精确纳秒下触发。系统会尽最大努力满足我们的需求,但并不能保证准确的触发时间。
当计算机进入睡眠状态时,所有定时器调度源都将暂停。当计算机唤醒时,这些定时器调度源也会自动唤醒。根据定时器的配置,这种性质的暂停可能会影响定时器下次触发的时间。如果使用dispatch_time
函数或者DISPATCH_TIME_NOW
常量设置定时器调度源,则定时器调度源使用默认系统时钟来确定何时触发。但是,计算机进入睡眠状态时,默认时钟不会前进。相比之下,当使用dispatch_walltime
函数设置定时器调度源时,定时器调度源将其触发时间追踪到挂钟时间。后一种选择通常适用于定时间隔相对较大的定时器,因为其可以防止事件时间之间出现太多漂移。
以下代码给出了一个定时器的例子,每30秒触发一次,误差值为1秒。由于定时器间隔相对较大,因此使用dispatch_walltime
函数创建调度源。定时器首次触发,随后的事件每隔30秒到达一次。
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);
}
}
虽然创建定时器调度源是接收基于时间的事件的主要方式,但还有其他选项可用。如果想在指定的时间间隔后执行一次block,则可以使用dispatch_after
或者dispatch_after_f
函数。该函数的行为与dispatch_async
函数非常相似,不同之处在于它允许指定将block提交到队列的时间值。时间值可以根据需要指定为相对或者绝对时间值。
从描述符中读取数据
要从文件或套接字读取数据,必须打开文件或者套接字并创建一个类型为DISPATCH_SOURCE_TYPE_READ
的调度源。我们指定的事件处理程序应该能够读取和处理文件描述符的内容。对于文件而言,这相当于读取文件数据(或数据的一个子集)并为应用程序创建适当的数据结构。对于网络套接字而言,这涉及处理新接收的网络数据。
每当读取数据时,都应该将描述符配置为使用非阻塞操作。尽管可以使用dispatch_source_get_data
函数来查看有多少数据可供读取,但在拨打电话时该函数返回的数字可能会与实际读取数据时返回的数字不同。如果底层文件被截断或发生网络错误,则阻塞当前线程的描述符读取可能会停止正在执行的事件处理程序,并阻止调度队列调度其他任务。对于串行队列,这可能会使队列死锁,即使是并发队列也会减少可以启动的新任务的数量。
以下代码显示了配置一个调度源以从文件读取数据的示例。在此示例中,事件处理程序将指定文件的全部内容读入缓冲区,并调用自定义函数来处理数据。(一旦读取操作完成,此函数的调用者将使用返回的调度源来取消它。)为确保在没有数据要读取时调度队列不被阻塞,本示例使用fcntl
函数配置文件描述符来执行非阻塞操作。安装在调度源上的取消事件处理程序确保在读取数据后关闭文件描述符。
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函数确定何时读取了足够的文件数据并且可以取消调度源。默认情况下,从描述符读取数据的调度源被配置为在读取数据的同时,还要重复调度其事件处理程序。如果socket连接关闭或已经读取完整个文件,则调度源将自动停止调度事件处理程序。如果不再需要调度源,可以直接取消它。
将数据写入到描述符
将数据写入文件或套接字的过程与读取数据的过程非常相似。在为写入操作配置描述符后,需要创建一个类型为DISPATCH_SOURCE_TYPE_WRITE
的调度源。一旦创建了该调度源,系统会调用我们的事件处理程序,使其有机会开始将数据写入文件或套接字。当完成数据写入时,使用dispatch_source_cancel
函数取消调度源。
每次写入数据时,都应该将文件描述符配置为使用非阻塞操作。尽管可以使用dispatch_source_get_data
函数来查看写入空间的可用空间,但该函数返回的值仅供参考,调用时返回的值可能和实际写入数据后的值不同。如果发生错误,将数据写入阻塞文件描述符可能会中止正在执行的事件处理程序并阻止调度队列调度其他任务。对于串行队列,这可能会使队列死锁,即使是并发队列也会减少可以启动的新任务的数量。
以下代码展示了使用调度源将数据写入文件的基本方法。创建新文件后,该函数将生成的文件描述符传递给它的事件处理程序。被放入文件的数据由MyGetData函数提供,可以使用它替换生成文件数据所需的任何代码。在数据写入文件之后,事件处理程序会取消调度源以防止再次调用它。调度源的持有者负责释放它。
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
。当文件被删除、 写入或重命名时,可以使用这种类型的调度源来接收通知。当文件元信息的特定类型(如大小和链接数量)发生变化时,也可以使用它来提醒用户。
注意:为调度源指定的文件描述符在调度源本身处理事件时必须保持打开状态。
以下代码显示了一个示例,该示例监听文件的名称变化并在发生变化时执行一些自定义行为。(我们将提供实际行为来代替示例中调用的MyUpdateFileName函数。)由于描述符是专门为调度源打开的,因此调度源包含一个关闭描述符的取消事件处理程序。由于示例创建的文件描述符与底层文件系统对象关联,所以可以使用相同的调度源来检查任意数量的文件名更改。
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;
}
监听信号
UNIX信号允许来自于应用程序域外的操作。应用程序可以接收许多不同类型的信号,从不可恢复的错误(例如非法指令)到关于重要信息(例如子进程退出时)的通知。通常情况下,应用程序使用sigaction
函数来安装信号处理函数,其会在信号到达时立即同步处理信号。如果只是希望得到一个信号到达通知而实际上不想处理该信号,则可以使用信号调度源异步处理信号。
信号调度源并不能替代使用sigaction
函数安装的同步信号处理程序。同步信号处理程序实际上可以捕获信号并防止其终止我们的应用程序。信号调度源允许只监听信号的到达。另外,不能使用信号调度源来检索所有类型的信号。具体而言,不能使用它们来监听SIGILL
,SIGBUS
和SIGSEGV
信号。
由于信号调度源在调度队列中是异步执行的,因此它们不会受到与同步信号处理程序相同的限制。例如,可以从信号调度源的事件处理程序调用的函数是没有限制的。这种增加灵活性的折衷是,信号到达的时间与调度源的事件处理程序被调用的时间之间可能会有一些延迟。
以下代码显示了如何配置好一个信号调度源来处理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);
}
}
如果我们正在开发自定义框架,使用信号调度源的优势在于我们的代码可以监听独立于任何与之相关联的应用程序的信号。信号调度源不会干扰其他调度源或应用程序可能安装的任何同步信号处理程序。
监听进程
进程调度源能够监听特定进程的行为并做出适当的响应。父进程可能使用这种类型的调度源来监听它创建的任何子进程。例如,父进程可以用它来监听子进程的死亡。同样,如过父进程退出,子进程可以使用它来监听其父进程并退出。
以下代码显示了安装调度源来监听父进程终止的步骤。当父进程死亡时,调度源设置一些内部状态信息,让子进程指定其应该退出。(我们自己的应用程序需要实现MySetAppExitFlag函数来设置适当的终止标志。)由于调度源自主运行,因此它持有自己本身,它也会在程序关闭的情况下取消并释放自身。
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_cancel
函数显式取消它们。取消调度源会停止传递新事件,并且不能撤销。因此,通常在取消调度源后就立即释放它,如下所示:
void RemoveDispatchSource(dispatch_source_t mySource)
{
dispatch_source_cancel(mySource);
dispatch_release(mySource);
}
取消调度源是一个异步操作。虽然在调用dispatch_source_cancel
函数后没有处理新事件,但仍然处理已由调度源处理的事件。完成处理任何最终事件后,调度源将执行其取消事件处理程序(如果有)。
取消事件处理程序是我们释放内存或清理调度源已获取的任何所有资源的机会。如果调度源使用描述符或者Mach端口,则必须提供取消事件处理程序以在发生取消事件时关闭描述符或销毁端口。其他类型的调度源不一定需要取消事件处理程序,如果将任何内存或数据与调度源相关联,则就应该提供。例如,如果将数据存储在调度源的上下文指针中,则应该提供取消事件处理程序。
暂停和恢复调度源
可以使用dispatch_suspend
和dispatch_resume
函数来暂停和恢复调度源事件的传递。这些方法增加和减少调度对象的暂停技术。因此,在事件传递恢复之前,必须在每次调用dispatch_suspend
函数后对应得调用一次dispatch_resume
函数。
暂停调度源后,在该调度源处于暂停状态时发生的所有事件都会累积,直到队列恢复。当队列恢复时,不是提交所有事件,而是在提交之前将事件合并为单个事件。例如,如果正在监听文件的名称变化,则提交的事件仅包含最后的名称更改。以这种方式合并事件可防止它们全部被提交到队列中,并在工作恢复时压倒我们的应用程序。
网友评论