繁体   English   中英

Win32 - 从标准输入读取超时

[英]Win32 - read from stdin with timeout

我正在尝试做一些我认为应该很简单的事情:从标准输入进行阻塞读取,但是如果没有可用数据,则在指定的时间间隔后超时。

在 Unix 世界中,使用select()很简单,但在 Windows 中不起作用,因为stdin不是套接字。 在不创建额外线程等的情况下,下一个最简单的选项是什么?

我正在使用针对 Win32 环境的视觉 C++。

到目前为止,我已经尝试过:

  1. 使用select (如果输入不是套接字则不起作用)

  2. 使用WaitForSingleObject(GetStdHandle(STD_INPUT_HANDLE)) - 雷米的第一个建议。 如果标准输入是控制台,当您调用它时,这似乎总是立即返回(其他人报告了同样的问题)

  3. 使用重叠的 IO 并执行WaitForSingleObject (雷米的第三个建议)。 在这种情况下,当输入来自控制台时,读取似乎总是阻塞 - 似乎stdin不支持异步 I/O。

目前我在想我唯一剩下的选择是创建一个线程来执行阻塞读取,然后发出一个事件信号,然后让另一个线程超时等待事件。

我不得不解决类似的问题。 在Windows上,它不像Linux那么容易或明显。 但是,这是可能的。 诀窍是Windows将控制台事件放在控制台输入事件队列中。 您必须过滤掉您不关心的事件,并且只处理您关心的事件(如按键)。

有关进一步阅读: 请参阅Win32控制台文档

以下是一些基于我正在研究的套接字和stdin多路复用器的主要调试示例代码:

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
}

这应该这样做:

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;
}

使用GetStdHandle + WaitForSingleObject工作正常。 但是在进入循环之前,一定要设置approriate标志并刷新控制台缓冲区。

简而言之(没有错误检查)

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);

您需要GetStdHandle函数来获取控制台的句柄,然后您可以使用WaitForSingleObject等待事件在该句柄上发生,并超时。

使用GetStdHandle()获取stdin句柄。 然后你可以:

  1. 使用WaitForSingleObject()我stdin句柄本身来检测何时有可供读取的控制台输入,然后根据需要从中读取。

  2. 在循环中的stdin句柄上使用GetNumberOfConsoleInputEvents()PeekConsoleInput()来确定何时有可用于读取的数据,然后根据需要从中读取。

  3. 使用包含事件句柄的OVERLAPPED结构的ReadFile() ,然后使用带有WaitForSingleObject()的事件句柄来检测读取是否超时。

在任何情况下,如果重定向stdin,请小心。 如果将其重定向到非控制台I / O的某些内容,则不能将GetStdHandle()句柄与控制台函数一起使用。 如果将其重定向到文件,则必须使用ReadFile()

如果有人正在编写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);
    }
}

现有答案没有解决标准输入是匿名 pipe 而不是控制台的情况。 在这种情况下, GetNumberOfConsoleInputEvents()之类的函数将返回0x6 (错误句柄),并且上述方法将不起作用。

就我而言,我正在尝试使用 stdin 和 stdout 来促进异步进程间通信,因此父进程(nodejs)将 stdin 和 stdout 作为带有子进程(c++)的匿名管道打开。 对于这种情况,标准输入的类型可以检测如下:

 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);
 }

然后,为了从这个输入 pipe 启用异步读取,我想出了两个想法:

方法 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资源。 出于这个原因,我想出了另一种方法。

方法2:使用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 是一个 function 设计用于在等待消息时保持响应(超时后):

//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);
    }
}

第二种方法将立即响应新的输入。 为了获得额外的稳健性,可能需要检查并确保 pipe 的内容以\n结尾(因此不会发送不完整的消息)并将消息推送到向量以便多个消息不会相互覆盖如果接收线程不能足够快地处理它们。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM