我正在尝试做一些我认为应该很简单的事情:从标准输入进行阻塞读取,但如果没有可用数据,则在指定的时间间隔后超时。
在 Unix 世界中,使用
select()
很简单,但在 Windows 中不起作用,因为 stdin
不是套接字。不创建额外线程等的下一个最简单的选项是什么?
我正在使用针对 Win32 环境的 Visual C++。
到目前为止我已经尝试过:
使用
select
(如果输入不是套接字则不起作用)使用
WaitForSingleObject(GetStdHandle(STD_INPUT_HANDLE))
。 - 雷米的第一个建议。如果标准输入是控制台,那么当您调用它时,这似乎总是立即返回(其他人报告了相同的问题)使用重叠 IO 并执行
WaitForSingleObject
(Remy 的第三个建议)。在这种情况下,当输入来自控制台时,读取似乎总是会阻塞 - 似乎 stdin
不支持异步 I/O。目前我认为我唯一剩下的选择是创建一个线程,该线程将执行阻塞读取,然后发出事件信号,然后让另一个线程等待超时事件。
我必须解决类似的问题。在 Windows 上,它不像 Linux 那样简单或明显。然而,这是可能的。诀窍在于 Windows 将控制台事件放入控制台输入事件队列中。您必须过滤掉您不关心的事件,只处理您关心的事件(例如按键)。
进一步阅读:请参阅 Win32 控制台文档
这里是一些基于我正在研究的套接字和标准输入多路复用器的主要调试示例代码:
void ProcessStdin(void)
{
INPUT_RECORD record;
DWORD numRead;
if(!ReadConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &record, 1, &numRead)) {
// hmm handle this error somehow...
return;
}
if(record.EventType != KEY_EVENT) {
// don't care about other console events
return;
}
if(!record.Event.KeyEvent.bKeyDown) {
// really only care about keydown
return;
}
// if you're setup for ASCII, process this:
//record.Event.KeyEvent.uChar.AsciiChar
} // end ProcessStdin
int main(char argc, char* argv[])
{
HANDLE eventHandles[] = {
GetStdHandle(STD_INPUT_HANDLE)
// ... add more handles and/or sockets here
};
DWORD result = WSAWaitForMultipleEvents(sizeof(eventHandles)/sizeof(eventHandle[0]),
&eventHandles[0],
FALSE,
1000,
TRUE
);
switch(result) {
case WSA_WAIT_TIMEOUT: // no I/O going on right now
break;
case WSA_WAIT_EVENT_0 + 0: // stdin at array index 0
ProcessStdin();
break;
case WSA_WAIT_EVENT_0 + 1: // handle/socket at array index 1
break;
case WSA_WAIT_EVENT_0 + 2: // ... and so on
break;
default: // handle the other possible conditions
break;
} // end switch result
}
使用
GetStdHandle
+ WaitForSingleObject
效果很好。但请确保在进入循环之前设置适当的标志并刷新控制台缓冲区。
简而言之(没有错误检查)
std::string inStr;
DWORD fdwMode, fdwOldMode;
HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(hStdIn, &fdwOldMode);
// disable mouse and window input
fdwMode = fdwOldMode ^ ENABLE_MOUSE_INPUT ^ ENABLE_WINDOW_INPUT;
SetConsoleMode(hStdIn, fdwMode);
// flush to remove existing events
FlushConsoleInputBuffer(hStdIn);
while (!abort)
{
if (WaitForSingleObject(hStdIn, 100) == WAIT_OBJECT_0)
{
std::getline(std::cin, inStr);
}
}
// restore console mode when exit
SetConsoleMode(hStdIn, fdwOldMode);
这应该可以做到:
int main()
{
static HANDLE stdinHandle;
// Get the IO handles
// getc(stdin);
stdinHandle = GetStdHandle(STD_INPUT_HANDLE);
while( 1 )
{
switch( WaitForSingleObject( stdinHandle, 1000 ) )
{
case( WAIT_TIMEOUT ):
cerr << "timeout" << endl;
break; // return from this function to allow thread to terminate
case( WAIT_OBJECT_0 ):
if( _kbhit() ) // _kbhit() always returns immediately
{
int i = _getch();
cerr << "key: " << i << endl;
}
else // some sort of other events , we need to clear it from the queue
{
// clear events
INPUT_RECORD r[512];
DWORD read;
ReadConsoleInput( stdinHandle, r, 512, &read );
cerr << "mouse event" << endl;
}
break;
case( WAIT_FAILED ):
cerr << "WAIT_FAILED" << endl;
break;
case( WAIT_ABANDONED ):
cerr << "WAIT_ABANDONED" << endl;
break;
default:
cerr << "Someting's unexpected was returned.";
}
}
return 0;
}
如果有人正在编写 chrome 本机消息传递主机并正在寻找解决方案来检查 stdin 上是否有任何输入而不阻塞,那么这很有效:
HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
int timer = GetTickCount();
while(timer+10000 > GetTickCount())
{
unsigned int length = 0;
DWORD bytesAvailable = 0;
PeekNamedPipe(hStdin,NULL,0,NULL,&bytesAvailable,NULL);
if(bytesAvailable > 0)
{
for (int i = 0; i < 4; i++)
{
unsigned int read_char = getchar();
length = length | (read_char << i*8);
}
for (int i = 0; i < length; i++)
{
msg += getchar();
}
timer = GetTickCount();
}
else
{
// nothing to read, stdin empty
Sleep(10);
}
}
现有的答案没有解决标准输入是匿名管道而不是控制台的情况。在这种情况下,像
GetNumberOfConsoleInputEvents()
这样的函数将返回 0x6
(错误句柄),并且上述方法将不起作用。
在我的例子中,我尝试使用 stdin 和 stdout 来促进异步进程间通信,因此父进程(nodejs)将 stdin 和 stdout 作为子进程(c++)的匿名管道打开。对于这种情况,可以按如下方式检测 stdin 的类型:
HANDLE stdinput = GetStdHandle(STD_INPUT_HANDLE);
if (stdinput == INVALID_HANDLE_VALUE) {
DWORD problem1 = GetLastError();
cout << "Failed to get input handle. " << (void*) problem1 << endl;
return(1);
}
DWORD fileType = GetFileType(stdinput);
if (fileType != FILE_TYPE_PIPE) {
cout << "Input type is not pipe. Instead: " << (void*) fileType << endl;
return(2);
}
然后,为了从这个输入管道启用异步读取,我想出了两个想法:
方法1:循环不断轮询可用输入
do {
DWORD bytesAvailable = 0;
BOOL success = PeekNamedPipe(stdinput, NULL, NULL, NULL, &bytesAvailable, NULL );
if (!success) {
cout << "Couldn't run PeekNamedPipe." << endl;
DWORD problem = GetLastError();
cout << "Error code: " << (void*)problem << endl;
}
char buf[bytesAvailable+1]; //mingw allows dynamic stack allocation. In Visual studio might need to allocate on heap.
if (bytesAvailable > 0) {
ReadFile(stdinput, buf, additionalBytesAvailable, NULL, NULL);
cout << "Received: " << buf << endl;
}
Sleep(10); //Small delay between checking for new input
} while(1);
方法 1 存在输入处理速度无法超过小延迟的问题。当然延迟可以缩短,但是线程会消耗更多的CPU资源。为此,我想出了一种替代方法。
方法二:使用ReadFile阻塞并发送到不同的线程进行处理。在输入处理线程中,等待输入时会阻塞:
do {
char firstChar[2]; //we will read the first byte not sure if it is null terminated...
//block until at least one byte is available
ReadFile(stdinput, firstChar, 1, NULL, NULL);
DWORD additionalBytesAvailable = 0;
BOOL success = PeekNamedPipe(stdinput, NULL, NULL, NULL, &additionalBytesAvailable, NULL );
if (!success) {
cout << "Couldn't run PeekNamedPipe." << endl;
DWORD problem = GetLastError();
cout << "Error code: " << (void*)problem << endl;
}
char buf[additionalBytesAvailable+2]; //mingw allows stack allocation.
buf[0] = firstChar[0];
buf[1] = '\0';
if (additionalBytesAvailable > 0) {
ReadFile(stdinput, buf+1, additionalBytesAvailable, NULL, NULL);
}
std::cout << count << " Read: " << buf << endl;
//write some data to a different thread that is still responsive
pthread_mutex_lock(&responsiveThreadLock);
mutexProtectedString = std::string(buf);
pthread_mutex_unlock(&responsiveThreadLock);
PostThreadMessage(handleOfResponsiveThread, WM_NEWINPUT, NULL, NULL);
} while(1);
在保持响应的线程中:
MSG msg;
do {
GetMessageWithTimeout(&msg, 1000);
if (msg.message == WM_NEWINPUT) {
std::string receivedStringCopy = "";
pthread_mutex_lock(&responsiveThreadLock);
receivedStringCopy = mutexProtectedString;
pthread_mutex_unlock(&responsiveThreadLock);
std::cout << "Received: " << receivedStringCopy << endl;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
std::cout << "Still responsive. " << endl;
} while(1);
GetMessageWithTimeout 是一个旨在在等待消息时保持响应(超时后)的函数:
//Wait upto timeoutMs milliseconds for a message.
//Return TRUE if a message is received or FALSE if the timeout occurs or there is an error.
BOOL GetMessageWithTimeout(MSG *msg, UINT timeoutMs)
{
//Check the message queue and return immediately if there is a message available
BOOL hasMessage = PeekMessage(msg, NULL, 0, 0, PM_REMOVE);
if (hasMessage) {
return(TRUE);
}
else {
//Any new messages that have arrived since we last checked the message queue will
//cause MsgWaitForMultipleObjects to return immediately.
//otherwise this will block the thread until a message arrives or timeout occurs
DWORD res1 = MsgWaitForMultipleObjects(0, NULL, FALSE, timeoutMs, QS_ALLINPUT);
if (res1 == WAIT_TIMEOUT) {
printf("!");
return(FALSE);
}
if (res1 == WAIT_OBJECT_0) {
//If we are here, there *should* be a message available. We can just get it with PeekMessage
hasMessage = PeekMessage(msg, NULL, 0, 0, PM_REMOVE);
return(hasMessage);
}
//If we get here, its because we have a WAIT_FAILED. Don't know why this would occur, but if it
//does, lets pause for a bit, so we don't end up in a tight loop
Sleep(100);
return(FALSE);
}
}
第二种方法将立即响应新的输入。为了获得额外的稳健性,可能需要检查并确保管道的内容以
\n
结尾(因此不会发送不完整的消息)并将消息推送到向量,以便多个消息不会覆盖一个消息如果接收线程无法足够快地处理它们,则另一个。
使用
GetStdHandle()
获取标准输入句柄。 然后您可以:
在标准输入句柄本身上使用
WaitForSingleObject()
来检测何时有控制台输入可供读取,然后根据需要从中读取。
在循环中的 stdin 句柄上使用
GetNumberOfConsoleInputEvents()
或 PeekConsoleInput()
来确定何时有数据可供读取,然后根据需要从中读取。
无论如何,如果 stdin 已被重定向,请小心。如果它被重定向到非控制台 I/O 的内容,则无法将
GetStdHandle()
句柄与控制台功能一起使用。 如果重定向到文件,则必须使用 ReadFile()
来代替。
您需要
GetStdHandle
函数来获取控制台的句柄,然后您可以使用 WaitForSingleObject
等待该句柄上发生事件,并设置超时。