【发布时间】:2019-01-30 23:35:19
【问题描述】:
创建和使用线程需要大量资源,因此通常会为异步任务重用线程池。将任务打包,然后“发布”到代理,该代理将把任务排入下一个可用线程。
这是调度队列(即 Apple 的 Grand Central Dispatch)和线程处理程序(Android 的 Looper 机制)背后的理念。
现在,我正在尝试自己动手。事实上,我正在填补 Android 中的一个空白,即有一个用于在 Java 中发布任务的 API,但在本机 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() 的调用始终成功,从而允许工作线程在队列已满时继续处理队列?
【问题讨论】:
-
Iirc 文件描述符可以标记为非阻塞,如果 FIFO 已满,这将导致写入失败并出现 EAGAIN。这可以通过 int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, 标志 | O_NONBLOCK);
-
@IGarFieldI 谢谢,但请考虑如果队列已满,这可能会导致发布的任务永远不会运行。这对于某些应用程序是可以的,但不适用于制作线程处理程序,您希望保证任务最终会运行。我想这个解决方案会以某种方式利用非阻塞写入。
-
我会避免使用管道在线程之间进行通信:您正在涉及线程间通信不需要的操作系统功能。例如,
std::queue和std::mutex具有相同的目的,具有更大的灵活性和更少的麻烦 -
@AndrewHenle 这大致就是我的意思。不是说管道有问题,但我需要一种不写入工人自己的作业队列的方法,而不会填满。
-
我认为如果不考虑生产者可以比消费者更快地生成数据,就无法设计系统。管道会因为操作系统的限制而阻塞,std::queue 最终会因为系统的限制而阻塞。使用消息传递时,据说队列应该始终为空:消费者必须比生产者快。如果发生相反的情况(即:已超过任意限制),您将面临错误情况,您必须处理它。如果你不这样做,就会出现问题。没有任何架构可以保证消息不受控制地堆积。
标签: c++ multithreading pipe file-descriptor android-looper