使用管道在C ++中创建调度队列/线程处理程序:FIFO溢出

时间:2019-01-30 23:35:20

标签: c++ multithreading pipe file-descriptor android-looper

线程在创建和使用时会占用大量资源,因此通常会将线程池重用于异步任务。打包任务,然后将其“发布”到代理,该代理将使任务排入下一个可用线程。

这是调度队列(即Apple的Grand Central Dispatch)和线程处理程序(Android的Looper机制)背后的想法。

现在,我正在尝试自己尝试。实际上,我在Android中填补了一个空白,那里有一个API可以用Java发布任务,而不是本机NDK。但是,我将尽可能地保持此问题平台的独立性。

管道是我的情况的理想选择。我可以轻松地轮询工作线程上pipe(2)的读取端的文件描述符,并通过写入写入端将任务从任何其他线程中排队。看起来像这样:

int taskRead, taskWrite;

void setup() {
    // Create the pipe
    int taskPipe[2];
    ::pipe(taskPipe);
    taskRead = taskPipe[0];
    taskWrite = taskPipe[1];

    // Set up a routine that is called when task_r reports new data
    function_that_polls_file_descriptor(taskRead, []() {
        // Read the callback data
        std::function<void(void)>* taskPtr;
        ::read(taskRead, &taskPtr, sizeof(taskPtr));

        // Run the task - this is unsafe! See below.
        (*taskPtr)();

        // Clean up
        delete taskPtr;
    });
}

void post(const std::function<void(void)>& task) {
    // Copy the function onto the heap
    auto* taskPtr = new std::function<void(void)>(task);

    // Write the pointer to the pipe - this may block if the FIFO is full!
    ::write(taskWrite, &taskPtr, sizeof(taskPtr));
}

此代码将std::function放在堆上,并将指针传递到管道。 function_that_polls_file_descriptor然后调用提供的表达式来读取管道并执行函数。请注意,此示例中没有进行安全检查。

这在99%的时间内效果很好,但是有一个主要缺点。管道的大小有限,如果管道已满,则对post()的调用将挂起。这本身并不是不安全的,直到在任务内 调用post()为止。

auto evil = []() {
    // Post a new task back onto the queue
    post({});
    // Not enough new tasks, let's make more!
    for (int i = 0; i < 3; i++) {
        post({});
    }

    // Now for each time this task is posted, 4 more tasks will be added to the queue.
});

post(evil);
post(evil);
...

如果发生这种情况,那么工作线程将被阻塞,等待写入管道。但是管道的FIFO已满,并且工作线程没有从中读取任何内容,因此整个系统处于死锁状态。

如何确保从工作线程总是退出对post()的调用成功,从而允许工作线程在队列已满的情况下继续处理队列?

4 个答案:

答案 0 :(得分:2)

感谢本文中的所有评论和其他答案,我现在对这个问题有一个可行的解决方案。

我采用的技巧是通过检查哪个线程正在调用post()来确定工作线程的优先级。这是粗略的算法:

pipe ← NON-BLOCKING-PIPE()
overflow ← Ø
POST(task)
    success ← WRITE(task, pipe)
    IF NOT success THEN
        IF THREAD-IS-WORKER() THEN
            overflow ← overflow ∪ {task}
        ELSE
            WAIT(pipe)
            POST(task)

然后在辅助线程上:

LOOP FOREVER
    task ← READ(pipe)
    RUN(task)

    FOR EACH overtask ∈ overflow
        RUN(overtask)

    overflow ← Ø

根据{Sigismondo的答案改编而成的pselect(2)执行等待。

这是在我的原始代码示例中实现的算法,该算法将适用于单个工作线程(尽管在复制粘贴之后我尚未对其进行测试)。通过为每个线程设置单独的溢出队列,可以将其扩展为用于线程池。

int taskRead, taskWrite;

// These variables are only allowed to be modified by the worker thread
std::__thread_id workerId;
std::queue<std::function<void(void)>> overflow;
bool overflowInUse;

void setup() {
    int taskPipe[2];
    ::pipe(taskPipe);
    taskRead = taskPipe[0];
    taskWrite = taskPipe[1];

    // Make the pipe non-blocking to check pipe overflows manually
    ::fcntl(taskWrite, F_SETFL, ::fcntl(taskWrite, F_GETFL, 0) | O_NONBLOCK);

    // Save the ID of this worker thread to compare later
    workerId = std::this_thread::get_id();
    overflowInUse = false;

    function_that_polls_file_descriptor(taskRead, []() {
        // Read the callback data
        std::function<void(void)>* taskPtr;
        ::read(taskRead, &taskPtr, sizeof(taskPtr));

        // Run the task
        (*taskPtr)();
        delete taskPtr;

        // Run any tasks that were posted to the overflow
        while (!overflow.empty()) {
            taskPtr = overflow.front();
            overflow.pop();

            (*taskPtr)();
            delete taskPtr;
        }

        // Release the overflow mechanism if applicable
        overflowInUse = false;
    });
}

bool write(std::function<void(void)>* taskPtr, bool blocking = true) {
    ssize_t rc = ::write(taskWrite, &taskPtr, sizeof(taskPtr));

    // Failure handling
    if (rc < 0) {
        // If blocking is allowed, wait for pipe to become available
        int err = errno;
        if ((errno == EAGAIN || errno == EWOULDBLOCK) && blocking) {
            fd_set fds;
            FD_ZERO(&fds);
            FD_SET(taskWrite, &fds);

            ::pselect(1, nullptr, &fds, nullptr, nullptr, nullptr);

            // Try again
            return write(tdata);
        }

        // Otherwise return false
        return false;
    }

    return true;
}

void post(const std::function<void(void)>& task) {
    auto* taskPtr = new std::function<void(void)>(task);

    if (std::this_thread::get_id() == workerId) {
        // The worker thread gets 1st-class treatment.
        // It won't be blocked if the pipe is full, instead
        // using an overflow queue until the overflow has been cleared.
        if (!overflowInUse) {
            bool success = write(taskPtr, false);
            if (!success) {
                overflow.push(taskPtr);
                overflowInUse = true;
            }
        } else {
            overflow.push(taskPtr);
        }
    } else {
        write(taskPtr);
    }
}

答案 1 :(得分:1)

您可以使用旧商品select来确定文件描述符是否准备好用于写入:

  

将监视writefds中的文件描述符以查看是否   有足够的空间用于写操作(尽管大写操作仍然会阻塞)。

由于您正在编写指针,因此write()根本无法归类为大。

很显然,您必须准备好处理帖子可能失败的事实,然后准备在以后重试...否则,您将面临无限增长的管道,直到系统再次崩溃。

或多或少(未经测试)

bool post(const std::function<void(void)>& task) {
    bool post_res = false;

    // Copy the function onto the heap
    auto* taskPtr = new std::function<void(void)>(task);

    fd_set wfds;
    struct timeval tv;
    int retval;

    FD_ZERO(&wfds);
    FD_SET(taskWrite, &wfds);

    // Don't wait at all
    tv.tv_sec = 0;
    tv.tv_usec = 0;

    retval = select(1, NULL, &wfds, NULL, &tv);
    // select() returns 0 when no FD's are ready
    if (retval == -1) {
      // handle error condition
    } else if (retval > 0) {
      // Write the pointer to the pipe. This write will succeed
      ::write(taskWrite, &taskPtr, sizeof(taskPtr));
      post_res = true;
    }
    return post_res;
}

答案 2 :(得分:1)

使管道写文件描述符不阻塞,以便在管道已满时write失败,并EAGAIN


一项改进是增加管道缓冲区的大小。

另一种方法是使用UNIX套接字/套接字对并增加套接字缓冲区的大小。

另一种解决方案是使用UNIX数据报套接字,许多工作线程可以从中读取该套接字,但是只有一个线程可以获取下一个数据报。换句话说,您可以将数据报套接字用作线程调度程序。

答案 3 :(得分:0)

如果你只看 Android/Linux 使用管道不是艺术的开始,但使用事件文件描述符和 epoll 是要走的路。