« popo 的语音通话 | 返回首页 | 临时决定出差 »

Windows 下以非阻塞方式读取标准输入

最近遇到一个小问题,游戏的 client 在开发调试阶段需要接收控制台的输入指令。这个需求其实一直都有,只不过以前是自己写的控制台,那样反而好控制一些。使用 Windows 标准控制台也不是第一次,但是这个输入问题都没有好好的解决。这次又碰到这个问题,决定找个好点的解决方案。

读取标准输入的 C 函数,像 scanf , gets 这些都是阻塞方式的。一经调用,程序就塞在那里不动了。起初的想法是,既然控制台输入就是一个标准输入文件,那么把这个文件修改成非阻塞模式就可以了。google 了一下,似乎 windows 下并没有 fcntl 或是 ioctl 这样的东西可以修改 stdin 为非阻塞模式。或许有别的 windows API 吧,没精力去查。

比较丑陋的方法是用 _kbhit 检测键盘输入,这个方法不太符合我的审美观。想了一下,觉得还是另外开个线程清爽一点。反正是调试用,虽然从资源占用与效率角度看不太美妙,姑且也可以凑合了。

以下代码可以工作 :)

#include <windows.h>
#include <process.h>
#include <stdio.h>

#define BUFFER_MAX 1024

char g_nbstdin_buffer[2][BUFFER_MAX];
HANDLE g_input[2];
HANDLE g_process[2];

DWORD WINAPI console_input(LPVOID lpParameter)
{
    for (;;) {
        int i;
        for (i=0;i<2;i++) {
            fgets(g_nbstdin_buffer[i],BUFFER_MAX,stdin);
            SetEvent(g_input[i]);
            WaitForSingleObject(g_process[i],INFINITE);
        }
    }
    return 0;
}

void create_nbstdin()
{
    int i;
    DWORD tid;
    CreateThread(NULL,1024,&console_input,0,0,&tid);
    for (i=0;i<2;i++) {
        g_input[i]=CreateEvent(NULL,FALSE,FALSE,NULL);
        g_process[i]=CreateEvent(NULL,FALSE,FALSE,NULL);
        g_nbstdin_buffer[i][0]='\0';
    }
}

const char* nbstdin()
{
    DWORD n=WaitForMultipleObjects(2,g_input,FALSE,0);
    if (n==WAIT_OBJECT_0 || n==WAIT_OBJECT_0+1) {
        n=n-WAIT_OBJECT_0;
        SetEvent(g_process[n]);
        return g_nbstdin_buffer[n];
    }
    else {
        return 0;
    }
}

void main()
{
    create_nbstdin();
    for (;;) {
        const char *line=nbstdin();
        if (line) {
            printf(">%s",line);
        }
        else {
            Sleep(0);
        }
    }
}

这个程序会用一个额外的线程去读取 stdin ,我实现了一个叫做 nbstdin() 的函数,其作用有点像 gets() 。但这个函数是非阻塞的,如果控制台没有新的行输入,它会返回一个空指针。

这个程序用了两个输入 buffer 乒乓切换,这样做可以避免在两次调用 nbstdin() 之间对输入 buffer 的处理被读线程破坏掉。

程序结束的时候并没有释放创建出来的 Event 也没有主动关闭读输入的子线程,我觉得这样做更简洁,该 os 处理的事情留给 os 吧 :)

Comments

那线程如何正常退出呢。

正好在学习怎么做一个C语言的聊天程序。但是等待用户输入程序会卡。参考一下

云风大哥,CRT相关的线程应该使用_beginthread来创建吧

我用_kbhit 封装了一个类实现的
讨厌多线程

使用异步API I/0函数可能效率更高、资源占用更低

还是使用overlapped I/O更好一些。

关注云风的BLOG好久.
从中受益不少好东西.

c++下面有没有比较好的gc库,可以动态内存整理的

c++为什么会把虚拟内存的东西再交换回来呢? 因为这种情况经常出现:调用 ~A 的时候需要 delete B 。B 的指针放在 A 的成员里,所以原本交换到外存的 A 对象就需要交换回内存,取回 B 指针只为了调用 ~B 。

解决这个问题的方案之一就是建立一个类似 gc 的资源管理方案,最后一次释放掉所有资源。apache 就有类似的实现。

to liaoliao: 我认为现在这样处理已经足够使用了,其实 console 会缓存阻塞住的键盘输入。如果真需要更流畅的解决 console 输入的话,应该在主线程中查询到新的 console 指令后就立刻取出来放进队列, 而不应该让输入线程开更多的 buffer 缓存更多的输入行。

目前,输入线程允许在主线程处理上一条指令的时候,输入新指令,对于人的键盘输入,速度已经够用了。

这个问题我以前碰到过,当时我想出一个办法:win上虽然不能改stdin的属性也没有现成的非阻塞读函数,但是win上对socket有一个select函数是非阻塞的,所以我就在主线程里建一个socket并在上面调用select函数,然后开另一个线程去用scanf读stdin并把读到的内容发往主线程里建的那个socket。这样主线程里的select就不会阻塞了。
方法虽然很笨,但也比较简单满足我的需要了~

两个输入 buffer 乒乓切换富有巧思。这样主线程是不会等工作线程,但工作线程却要等主线程吧?比如控制台输入消息后,得等主线程处理完后才能输下一条消息。要互不等待的话,是不是该引入消息队列,有没有更好的办法?

c++为什么会把虚拟内存的东西再交换回来呢?

是啊,我就奇怪,为什么很多程序,尤其是游戏,运行的时候速度还可以,退出的时候就要延时很久很久。
退出的时候无非就是释放资源和内存。如果一个程序用了上G的虚拟内存,可能运行的时候并不慢,因为这些虚拟内存几乎很少用到,很少交换到物理内存,但是调用C++的delete或者相似的东西的时候就惨了,就非得把这上G的虚拟内存一一交换到物理内存来,再依次调用析构函数,而析构函数里面其实多半也就是delete别的对象,导致的结果操作系统被迫对页面文件进行上G的IO

假如根本不析构,直接结束掉进程,操作系统释放位于虚拟内存中的进程所占用的内存,操作系统只需要把页面文件的这些部分标记成已经释放就行了,几乎没有IO开销。

所以我深深地怀疑C++的析构在进程退出的时候是否有必要,是否起了副作用

Post a comment

非这个主题相关的留言请到:留言本