退出应用程序时stdin在Windows上阻塞

6
我有一个应用程序,它使用getline()在一个线程中从标准输入读取数据。我想要在主线程关闭应用程序,而getline仍然阻塞另一个线程。如何实现这一点?
我不想强制用户按ctrl-Z关闭stdin和应用程序。
到目前为止,我已经尝试过我的编译器设置(RuntimeLibrary=/MT)在Windows 8.1 64位、v120平台工具集上:
- freopen stdin,但被内部锁定阻止 - 销毁调用abort()的线程 - putback一个Eof、行结束符到std::cin,也被阻塞
* 更新 *
- detach()不起作用,exit()被锁定 - winapi TerminatThread()调用abort() - winapi CloseHandle(GetStdHandle(STD_INPUT_HANDLE))挂起 - 调用TerminateProcess() - 可以工作,但我希望优雅地退出
* 更新2:解决方案 *
- WriteConsoleInput()可以使std::getline()从阻塞读取中返回。这适用于任何msvc运行时库。有关工作解决方案的代码,请参见接受的答案。
下面是示例代码,显示了问题:
#include <iostream>
#include <thread>
#include <string>
#include <chrono>

int main(int argc, char *argv[])
{
    bool stop = false;
    std::thread *t = new std::thread([&]{
        std::string line;
        while (!stop && std::getline(std::cin, line, '\n')) {
            std::cout << line;
        }
    });

    std::this_thread::sleep_for(std::chrono::seconds(1));

    stop = true;
    // how to stop thread or make getline to return here?

    return 0;
}

如果在调用detach()join()之前没有销毁线程,那么线程的析构函数将会调用std::terminate。 - Ivan Aksamentov - Drop
你为什么要进行动态分配? - RamblingMad
如果是针对Windows的话,您可以使用CancelIOEx:https://devblogs.microsoft.com/oldnewthing/20150323-00/?p=44413 - EricLaw
7个回答

2

使用writeConsoleInput()函数可以使std::getline从阻塞读取中返回,因此即使使用/MT编译器选项也可以解决这个问题。

#include <Windows.h>

#include <iostream>
#include <thread>
#include <string>
#include <chrono>
#include <atomic>

int main(int argc, char *argv[])
{
    std::atomic_bool stop;

    stop = false;

    std::thread t([&]{
        std::string line;
        while (!stop.load() && std::getline(std::cin, line, '\n')) {
            std::cout << line;
        }
    });


    std::this_thread::sleep_for(std::chrono::seconds(1));

    stop = true;

    DWORD dwTmp;
    INPUT_RECORD ir[2];
    ir[0].EventType = KEY_EVENT;
    ir[0].Event.KeyEvent.bKeyDown = TRUE;
    ir[0].Event.KeyEvent.dwControlKeyState = 0;
    ir[0].Event.KeyEvent.uChar.UnicodeChar = VK_RETURN;
    ir[0].Event.KeyEvent.wRepeatCount = 1;
    ir[0].Event.KeyEvent.wVirtualKeyCode = VK_RETURN;
    ir[0].Event.KeyEvent.wVirtualScanCode = MapVirtualKey(VK_RETURN, MAPVK_VK_TO_VSC);
    ir[1] = ir[0];
    ir[1].Event.KeyEvent.bKeyDown = FALSE;
    WriteConsoleInput(GetStdHandle(STD_INPUT_HANDLE), ir, 2, &dwTmp);

    t.join();

    return 0;
}

这是一个有效的替代方案,但请注意:除非进程“附加到控制台”(或任何术语),否则此方法将无法工作。 - helmesjo

0

只需分离线程:

#include <iostream>
#include <thread>
#include <chrono>

bool stop = false;
int main(int argc, char *argv[])
{
    std::thread t([]{
        bool stop = false;
        std::string line;
        while (!stop && std::getline(std::cin, line, '\n')) {
            std::cout << line;
        }
    });

    std::this_thread::sleep_for(std::chrono::seconds(1));

    stop = true;
    // Without detach: g++: terminate called without an active exception
    t.detach();

    return 0;
}

更好的方法包括:

  • 如果stdin正在获取用户输入,请在线程中进行适当的退出(不要突然终止交互式输入)
  • 从stdin进行非阻塞读取(这取决于系统)
  • 设置管道
  • 使用套接字

1
已经尝试过了。应用程序不会以这种方式退出。在_locterm()中等待锁定。 - simon
我通过让负责读取标准输入的线程在ReadFile()中阻塞,而非getline()中,成功实现了这种方法。对我来说似乎工作得不错(尽管我认为像这样保留一个运行中的线程是可怕的hack)。相关代码可以从此文件的第28行开始看到:https://public.msli.com/lcs/muscle/muscle/dataio/StdinDataIO.cpp - Jeremy Friesner

0

目前没有标准的、甚至是跨平台的解决方案来中断std:cinstd::thread。在这两种情况下,您需要使用特定于操作系统的API。您可以使用std::thread::native_handle()获取线程的特定于操作系统的句柄。

作为一种快速而简单的方法,您可以只是分离线程。但要注意这个那个

int main(int argc, char *argv[]) {
    std::thread t([&] {
        std::string line;
        while (std::getline(std::cin, line, '\n')) {
            std::cout << line;
        }
    });
    t.detach();

    std::this_thread::sleep_for(std::chrono::seconds(1));
}

另外:

  • 不需要在堆上分配线程:

    std::thread t([]{
    
    });
    
  • C++ 中的 return 0; 是不必要的
  • stop = true; 会触发编译错误,因为 stop 在此作用域中未声明
  • 如果您计划以这种方式共享布尔标志,则会出现典型的竞争条件,从而引起UB
  • 可能是非阻塞输入的“标准”或“跨平台”解决方案的最接近实现方式可能是 ncurses(适用于 *nix,并在 Windows 上使用 pdcurses

好的,编辑太多了...我修复了示例,使其可以编译。 - simon
@simon,具体是什么不起作用?“退出被锁定”是什么意思?对我来说,在Linux(GCC 4.9.2)和Windows(VS2013u5)上,程序在1秒内启动并退出。您使用的平台、编译器和标准库是什么?您是否逐字尝试过我的代码片段? - Ivan Aksamentov - Drop
是的,我已经逐字尝试过了。退出被锁定意味着我在调用堆栈中看到退出函数被调用,但其中一个被调用的函数等待释放锁。VS2013 v120 - simon
@Simon,你能把整个解决方案文件夹发给我吗?这样我就可以在我的电脑上检查它了。 - Ivan Aksamentov - Drop
我尝试了一个新的干净解决方案,你的代码在那里运行良好。不幸的是,我的项目必须使用C++/CodeGeneration/RuntimeLibrary=/MT,并且使用相应的运行时库时它无法退出。 - simon

0

如果其他方法都不起作用,总还有核选项:

TerminateProcess(GetCurrentProcess(), 0);

请确保您已清空任何关心的运行时缓冲区。


使用/MT编译器选项也会被阻止。程序无法退出。 - simon
1
对我来说没问题,即使使用/MT。你运行的是哪个操作系统? - Harry Johnston
抱歉,你是对的。即使使用/MT,这个也能正常工作。 - simon

0

这个代码是有多线程缺陷的。首先,为什么要在堆上创建新线程?只需声明它在栈上并调用 std::thread::detach
其次,谁保证了在此情况下的 stop 可行?很可能处理器会缓存此布尔值并从未查看真实值(如果没有将其部分优化或其他编译技巧...)。你需要把它变成原子操作:

int main(int argc, char *argv[])
{
    std::atomic_bool stop;
    stop = false;
    std::thread t([&]{
        std::string line;
        while (!stop.load() && std::getline(std::cin, line, '\n')) {
            std::cout << line;
        }
    });
    t.detach();
    stop = true;
}

使用Visual Studio 2013在Windows 7上编译,按预期工作。


那只是一个示例代码,展示我想要完成的任务。你的代码不会使用/MT编译器开关退出,而我必须使用它。 - simon
所以你的问题不在那里。你真正的线程是静态的吗?它属于某个类吗? - David Haim

0

这对我来说可行,尽管有点不太靠谱:

#include <Windows.h>

#include <iostream>
#include <thread>
#include <string>
#include <chrono>
#include <atomic>

int main(int argc, char *argv[])
{
    std::atomic_bool stop;

    stop = false;

    std::thread t([&]{
        std::string line;
        while (!stop.load() && std::getline(std::cin, line, '\n')) {
            std::cout << line;
        }
    });

    std::this_thread::sleep_for(std::chrono::seconds(1));

    stop = true;

    CloseHandle(GetStdHandle(STD_INPUT_HANDLE));

    t.join();

    return 0;
}

对我不起作用。CloseHandle调用没有返回。 - simon
你能否在一个新安装的虚拟机上尝试一下?我在想可能有第三方软件(例如反病毒软件)在干扰。据我所知,CloseHandle在任何情况下都不应该被阻塞。 - Harry Johnston
另一种方法可能是通过WriteConsoleInput插入输入字符,以便getline自然退出。但这样做会很混乱。 - Harry Johnston

0

我曾经也遇到过同样的问题,主要是由于链接静态运行时(/MT)。我从这里和那里收集了一些碎片,并将其包装在一个简单易用的RAII对象中,以便为我完成此操作(显然,由于Windows.h的原因,这不在任何头文件中):

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

#else
#include <pthread.h>
#endif

struct forcefully_stop_thread_on_destruction
{
    forcefully_stop_thread_on_destruction(std::thread&& thread, bool isBlockedByStdin) :
        thread_(std::move(thread)),
        isBlockedByStdin_(isBlockedByStdin)
    {}
    ~forcefully_stop_thread_on_destruction()
    {
#ifdef _WIN32
        // Main issue on Windows is where we link the static runtime (/MT) which locks the stdin file,
        // so it doesn't matter if we read stdin on background thread, it still deadlocks the process on exit & even terminate.

        if (isBlockedByStdin_)
        {
            // On windows, if a console is attached, write to stdin so that std::getline(..) unblocks, and thread bails out naturally.
            CONSOLE_SCREEN_BUFFER_INFO csbi;
            const bool hasConsole = ::GetConsoleScreenBufferInfo(::GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
            if (hasConsole)
            {
                DWORD dwTmp;
                INPUT_RECORD ir[2];
                ir[0].EventType = KEY_EVENT;
                ir[0].Event.KeyEvent.bKeyDown = TRUE;
                ir[0].Event.KeyEvent.dwControlKeyState = 0;
                ir[0].Event.KeyEvent.uChar.UnicodeChar = VK_RETURN;
                ir[0].Event.KeyEvent.wRepeatCount = 1;
                ir[0].Event.KeyEvent.wVirtualKeyCode = VK_RETURN;
                ir[0].Event.KeyEvent.wVirtualScanCode = ::MapVirtualKey(VK_RETURN, MAPVK_VK_TO_VSC);
                ir[1] = ir[0];
                ir[1].Event.KeyEvent.bKeyDown = FALSE;
                ::WriteConsoleInput(::GetStdHandle(STD_INPUT_HANDLE), ir, 2, &dwTmp);

                // Wait for blocking read to release and thread finish execution.
                thread_.join();
            }
            // No console = no reliable way to unblock stdin
            else
            {
                // WE ARE GOING NUCLEAR AT THIS POINT
                // No console, so we can't release blocking stdin read: Kill whole process. Sigh.

                struct terminate_process
                {
                    ~terminate_process()
                    {
                        TerminateProcess(GetCurrentProcess(), 0);
                    }
                };
                // Instantiate in "static storage" so that termination happens as late as possible (after main() returns)
                static terminate_process nuclear;
                // Don't wait for blocking read to release
                thread_.detach();
            }
        }
        else
        {
            thread_.join();
        }
#else
        // On unix, forcefully terminate thread.
        if (isBlockedByStdin_)
        {
            pthread_cancel(thread_.native_handle());
        }
        // Wait for blocking read to release and thread finish execution.
        thread_.join();
#endif
    }
private:
    std::thread thread_;
    bool isBlockedByStdin_;
};

使用示例:

auto thread = std::thread([buff = inputStream.rdbuf()](){
    std::string input;
    std::istream inputStream(buff);
    while (true)
    {
        std::getline(inputStream, input);
        // Use input
    }
});
// `inputStream` can be any stream, so verify it's stdin since that's the problem.
const auto isBlockedByStdin = inputStream.rdbuf() == std::cin.rdbuf();
auto handleProblems = forcefully_stop_thread_on_destruction(std::move(thread), isBlockedByStdin);
// Hold on to `handleProblems` until done polling stdin.

本质上:

if(windows && hasConsole)
{ 
    /* Write to console to unblock stdin */ 
}
else if(windows && !hasConsole)
{ 
    /* Terminate process with exit code 0 after main() has exit (before hitting deadlock) */
}
else
{ 
    /* Assume "Unix" & call pthread_cancel */ 
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接