【问题标题】:Asynchronous I/O in C with a callback function -- exactly when and how does I/O thread return?带有回调函数的 C 中的异步 I/O —— I/O 线程究竟何时以及如何返回?
【发布时间】:2021-09-19 09:00:52
【问题描述】:

我的问题是,当涉及回调函数时,异步 I/O 调用中的 I/O 线程究竟何时返回。具体来说,鉴于这个用于读取文件的非常通用的代码......

#include<stdio.h>
#include<aio.h>

...

// callback function:
void finish_aio(sigval_t sigval) {

/* do stuff ... maybe close the file */

}

int main() {

    struct aiocb my_aiocb;
    int aio_return;
...
//Open file, take care of any other prelims, then
//Fill in file-specific info for my_aiocb, then
//Fill in callback information for my_aiocb:
    my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;
    my_aiocb.aio_sigevent.sigev_notify_function = finish_aio;
    my_aiocb.aio_sigevent.sigev_notify_attributes = NULL;
    my_aiocb.aio_sigevent.sigev_value.sival_ptr = &info_on_file;

// then read the file:
    aio_return = aio_read(&my_aiocb);
    
// do stuff that doesn't need data that is being read ...
// then block execution until read is complete:
    while(aio_error(&my_aiocb) == EINPROGRESS) {}
// etc.
}

我了解一旦文件读取完成,就会调用回调函数。但究竟会发生什么呢? I/O 线程是否开始运行回调finish_aio()?或者它会在返回主线程时生成一个新线程来处理该回调?另一种说法是:aio_error(&amp;my_aiocb) 何时停止返回EINPROGRESS?是在调用回调之前,还是在回调完成时?

【问题讨论】:

  • 哪个操作系统? Linux?如果你需要异步 I/O,我强烈推荐使用 epoll。 man aio:“当前的 Linux POSIX AIO 实现是由 glibc 在用户空间中提供的。这有许多限制,最明显的是维护多个线程来执行 I/O 操作的成本很高且扩展性很差。”

标签: c multithreading asynchronous io


【解决方案1】:

我了解一旦文件读取完成,就会调用回调函数。但究竟会发生什么?

发生的情况是,当 IO 完成时,它“表现得好像”它启动了一个新线程(类似于调用 pthread_create(&amp;ignored, NULL, finish_aio, &amp;info_on_file))。

aio_error(&amp;my_aiocb)什么时候停止返回EINPROGRESS

我希望aio_error(&amp;my_aiocb) 在 IO 完成后立即停止返回 EINPROGRESS,然后系统(可能是标准库)要么开始创建一个新线程来调用 finish_aio(),要么“解除阻塞”一个“以前在您不知情的情况下创建”线程。但是,我不认为任何地方都记录了确切的顺序(“定义的实现”),因为无论如何从 finish_aio() 以外的任何地方调用 aio_error(&amp;my_aiocb) 都没有多大意义。

更具体地说;如果您正在使用轮询 (my_aiocb.aio_sigevent.sigev_notify = SIGEV_NONE),那么您将自己反复检查 aio_error(&amp;my_aiocb),并且您不在乎在此之前或之后是否收到通知,因为您根本没有收到通知;如果您不使用轮询,您将等到(通过新线程或信号)通知您有理由检查aio_error(&amp;my_aiocb)

换句话说,您的finish_aio() 看起来更像这样:

void finish_aio(sigval_t sigval) {
    struct aiocb * my_aiocb = (struct aiocb *)sigval;
    int status;

    status = aio_error(&my_aiocb);

    /* Figure out what to do (handle the error or handle the file's data) */

.. 对于main()while(aio_error(&amp;my_aiocb) == EINPROGRESS)(这可能会浪费大量的 CPU 时间)将被删除和/或可能被其他东西替换(例如,pthread_cond_wait() 等到代码finish_aio() 执行 pthread_cond_signal() 告诉主线程它可以继续)。


为了理解这一点,让我们看一下纯轮询的样子:

int main() {

    struct aiocb my_aiocb;
    int aio_return;
    ...
    //Open file, take care of any other prelims, then
    //Fill in file-specific info for my_aiocb, then

    my_aiocb.aio_sigevent.sigev_notify = SIGEV_NONE;   /* CHANGED! */
    // my_aiocb.aio_sigevent.sigev_notify_function = finish_aio;
    // my_aiocb.aio_sigevent.sigev_notify_attributes = NULL;
    // my_aiocb.aio_sigevent.sigev_value.sival_ptr = &info_on_file;

    // then read the file:
    aio_return = aio_read(&my_aiocb);

    // do stuff that doesn't need data that is being read ...
    // then block execution until read is complete:
    while(aio_error(&my_aiocb) == EINPROGRESS) {}
    finish_aio(sigval_t sigval);           /* ADDED! */
}

在这种情况下,它的行为几乎与您的原始代码相同,只是没有额外的线程(而且您不必关心“不存在的线程”是在 aio_error(&amp;my_aiocb) 返回值之前还是之后启动的除了EINPROGRESS)。

纯轮询的问题在于,while(aio_error(&amp;my_aiocb) == EINPROGRESS) 可能会浪费大量 CPU 时间不断检查何时什么都没有发生。

使用my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD 的主要目的是避免在没有任何变化的情况下浪费大量的 CPU 时间轮询(不要忘记在某些情况下,像这样浪费 CPU 时间轮询会阻止其他线程,包括 finish_aio() 线程,从获取 CPU 时间)。换句话说,您想删除while(aio_error(&amp;my_aiocb) == EINPROGRESS) 循环,所以您使用了SIGEV_THREAD,以便您可以删除该轮询循环

新的问题是(如果主线程必须等到数据准备好)你需要一些其他的方式让主线程等到数据准备好。但是,通常您真正关心的不是“aio_read() 完成”,而是其他东西。例如,原始文件数据可能是文本文件中的一堆值(如“12、34、56、78”),您想要解析该数据并创建一个整数数组,并想要通知主线程整数数组已准备就绪(如果您开始解析文件的数据,不想通知主线程)。可能是这样的:

int parsed_file_result = 0;

void finish_aio(sigval_t sigval) {
    struct aiocb * my_aiocb = (struct aiocb *)sigval;
    int status;

    status = aio_error(&my_aiocb);
    close(my_aiocb->aio_fildes);
    if(status == 0) {
        /* Read was successful */
        parsed_file_result = parse_file_data(); /* Create the array of integers */
        
    } else {
        /* Read failed, handle the error somehow */

        parsed_file_result = -1;  /* Tell main thread we failed to create the array of integers */
    }
    /* Tell the main thread it can continue somehow */
}

告诉主线程它可以继续(在finish_aio() 的末尾)的最好方法之一是使用pthread 条件变量(例如pthread_cond_signal()finish_aio() 的末尾调用;在pthread_cond_wait() 中主线程)。在这种情况下,主线程将简单地阻塞(内核/调度程序在调用 pthread_cond_signal() 之前不会给它任何 CPU 时间),因此不会浪费 CPU 时间轮询。

遗憾的是,pthread 条件变量并非微不足道(它们需要互斥体、初始化等),并且在这里教授/展示它们的使用与原始主题有点太远了。幸运的是;在其他地方找到一个好的教程应该不会有太大的麻烦。

重要的是,如果你使用了SIGEV_THREAD(这样你就可以删除那个糟糕的while(aio_error(&amp;my_aiocb) == EINPROGRESS) 轮询循环),在finish_aio() 已经启动之前,你没有理由调用aio_error(&amp;my_aiocb) ;并且没有理由关心 aio_error(&amp;my_aiocb)finish_aio() 启动之前是否已更改(或不更改)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-17
    • 1970-01-01
    • 2017-11-11
    • 1970-01-01
    • 2011-11-25
    • 2013-06-19
    相关资源
    最近更新 更多