[英]Clean way to stop/terminating a thread waiting on stdin in C++
我有一个线程正在等待用户输入。 如果我的主函数终止,我想通知用户输入线程终止,以便我可以加入该线程。
有没有比使用中断更干净/更好的方法?
任务队列呢?
伪代码:
用户输入线程
void user_input_thread_function()
{
Task* task;
for(;;)
{
queue.wait_dequeue(task);
if (!task)
{
// task is nullptr, a signal to stop gracefully
break;
}
// main thread did not yet ended and sent a valid task
// do something with task
delete task;
}
// do necessary things before stopping
}
主线程
// queue should be visible to both threads
QueueType queue;
int main()
{
thread user_input_thread(user_input_thread_function);
for(;;)
{
queue.enqueue(new Task("data"));
if (input exhausted)
queue.enqueue(nullptr);
}
// join all threads at the end
user_input_thread.join();
// you might want to create similar communication with the UDP thread using another queue
return 0;
}
对于队列,您可能希望使用现有解决方案。 这是我最近用于 1-1 线程通信的一个。 GitHub 链接它是在 Simplified BSD License 下授权的,所以我认为你会没事的。
如果我的
main
函数终止,我想通知用户输入线程终止,以便我可以加入该线程。
当main
函数返回 C++ 运行时调用std::exit.
std::exit
终止整个进程及其所有线程。 可以从任何调用std::exit
以终止整个进程。
根据这个问题,我尝试使用 std::async 构建一个非阻塞的 cin:
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
static std::string getAnswer()
{
std::string answer;
std::cout << "waiting on stdin" << std::endl;
std::cin >> answer;
return answer;
}
int main()
{
std::chrono::seconds timeout(5);
std::string answer = "default"; //default to maybe
while(true) {
std::cout << "new loop" << std::endl << std::flush;
std::future<std::string> future = std::async(getAnswer);
if (future.wait_for(timeout) == std::future_status::ready) {
answer = future.get();
}
std::cout << "Input was: " << answer << std::endl;
}
exit(0);
}
我在标准输出上得到的是:
new loop
waiting on stdin
Input was: default // after 5 sec
循环不会再次开始。 相反,当我在键盘上输入内容时,循环再次开始。 有人可以向我解释这种行为吗?
一些普遍的想法:
演员模型
阻止等待来自 stdin 的输入是一件好事。 这同样适用于您暗示的 UDP 套接字。
stdin 和套接字都是流,您可能会将其分解为离散消息。 但是等待来自文件描述符的数据的方法是 select() 或 epoll() 或类似的(取决于您的平台;请注意,Windows 为您提供了一个仅适用于套接字的选择...)。
无论如何,我的观点是,通过使用 select() 或 epoll(),您将走向 Actor 模型架构。 真正的问题是,在整个应用程序中是否有一个完整的猪? 我经常发现最简单的答案是“是”。
这很好,因为这样你的线程就可以在 stdin 和管道上选择(),主线程将在其中写入“退出”消息。 当线程发现管道准备好读取时,它会读取它,看到“退出”消息,然后干净利落地关闭,一切都井井有条。 当 select() 说有东西要读时,线程只会读取标准输入。
*nixes 采用的“一切都是文件描述符”的方法使得在 select() 或 epoll() 中包含几乎所有可以想象的数据源变得非常容易。
如今,Linux 甚至在 fd 上提供信号。 从 fd 中读取信号并在主循环中同步处理它们要容易得多,而不是在处理程序中异步处理(这实际上不能做太多)。
反应器与前摄器
Actor 模型架构是Reactors 。 您的进程/线程有一个循环,循环顶部有类似 select() 之类的东西,您可以从任何准备读取的 fd 读取输入并相应地处理该输入。 (其他反应堆架构包括通信顺序进程,如在 Rust 和 Go-lang 中发现的,erlang 也是如此)。
另一种选择是Proactors ,即在任何输入出现之前主动决定要做什么。 这在回调、期货、异步等领域非常稳固。你上面的非阻塞 cin 答案是 proactor。
现在有很多东西是 Proactor。 Windows 是,Boost ASIO 是(因为 Windows 是),等等。
尽量不要混用 Proactor 和 Reactor
问题是,试图将 Reactor 架构与 Proactor 架构混合可能会陷入困境。 这通常是可怕的。 所以,我的建议是,选择一个(reactor 或 proactor),坚持下去,不要试图将两者混合在一起。
这意味着预先非常仔细地考虑(例如,我是否必须在 Windows 上执行此操作,或者我将永远必须将其移植到 Windows 等)。
我的偏好
通常我更喜欢 Reactor。 使用 Proactor,您必须在知道是否有任何内容要读取之前启动诸如套接字读取之类的事情,从而导致您试图避免的确切问题(一个输入读取函数被阻塞并且永远不会解除阻塞在某些情况下,需要脏关机)。
在 Reactor 中,应用程序永远不会调用该 read 函数,直到它知道有一些东西要读取并且线程状态使得读取应该发生(例如,没有收到“退出”命令)。
Pro/Reactor 不等价
另一方面是在 Reactor 框架之上实现 Proactor 行为是可能的,但反过来就不可能了。
这方面的好例子包括 ZMQ。 ZMQ 本质上是一个 Actor 模型架构,一个围绕 zmq_poll() 构建的 Reactor。 ZMQ 有很多 Windows 绑定,它们在顶部呈现 Proactor 风格的外观。
ZMQ 只能在 Windows 上运行套接字(因为 Windows 只为套接字提供 select() ),但不能在 Windows 管道上工作(管道没有 select())。 在 Unix 中,ipc:// 传输支持管道。 在 Windows 上的进程中缺少管道并不是太糟糕,因为 inproc:// 传输确实有效。
类似地,Boost ASIO 之所以是前摄器,仅仅是因为在 Windows 上实现反应器的难度,同时包含了 Windows 上的所有 IPC,如管道、套接字和串行端口(ZMQ 选择不这样做)。 在 *nix 上,Boost ASIO 呈现了一个前摄器前端,但在引擎盖下它是使用 epoll() 实现的...
ZMQ
说到 ZMQ,如果您正在使用 *nix,我强烈建议您使用 ZMQ 作为您的 IPC 库。 它在使 ipc、sockets 之类的东西真正易于使用方面做得非常出色,并且您可以轻松地将诸如等待普通文件描述符(如 stdin)之类的东西集成到对 zmq_poll() 的调用中。 此外,许多其他框架,例如 GUI 框架,允许您将 fd 作为输入到它们自己的事件循环中,这可以包括 ZMQ 为您提供的“发生了一些事情”的 fd。 因此,将 ZMQ 处理通信集成到 gui 应用程序中相对容易。
如果您在 Windows 上使用它,则不可能以真正的 reactor 风格将其与 stdin 集成。
通史
当 cygwin 人开始在 Windows 上为他们的库实现 select() 时,他们遇到了一个可怕的问题,那就是不可能有一个可以等待套接字、串行端口、stdin、管道等的适当阻塞 select()。最后,他们通过为每个非套接字文件描述符设置一个线程来实现它,循环旋转测试 Windows 设备句柄以查看是否发生了任何事情。 这是非常低效的。
Boost ASIO 出现了 proactor 是因为希望在 Windows 上运行 Boost。
Windows(除了套接字)从一开始就是未经改造的前摄器,可能一直到内核和设备驱动程序。 但是,WSL 的第一个版本(WSL 目前非常流行)实现了 Linux 系统调用 shim; 即调用 select() 的 Linux 程序最终会调用 NT 内核来调用等效的函数。
这意味着管道,至少在 WSL 1.0 中,可以在 select() 中工作,但它会是一个 NT 内核调用来实现它。 这意味着reactor 的某些元素已经在某个地方进入了Windows,但尚未在Win32 / C,C++ 级别公开。
作为果断的前摄者,Windows 很容易让人联想到古代 Unix,它也不能执行 select()。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.