线程在创建和使用时会占用大量资源,因此通常会将线程池重用于异步任务。打包任务,然后将其“发布”到代理,该代理将使任务排入下一个可用线程。
这是调度队列(即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()
的调用成功,从而允许工作线程在队列已满的情况下继续处理队列?
答案 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 是要走的路。