使用多线程在 C 中实现 KeyPress 事件

Implementing a KeyPress Event in C with Multiple Threads

我的目标:线程将等待(忙循环不休眠)直到按下特定键(假设为 0)。每个线程都有一个不同的键,该键将触发该线程退出等待并继续执行等待后的命令。

我尝试了以下方法来实现:

使用 conio.h 和 getch() 但这是旧的并且不再适用于 gcc。资料来源:Why can't I find <conio.h> on Linux?

使用 ncurses.h 和 getch() 但这会在等待键盘按下时停止执行。 我使用的代码:http://tldp.org/HOWTO/NCURSES-Programming-HOWTO/scanw.html#GETCHCLASS

我当前使用 termios.h 的实现:

主要内容:

      //Keypress Event Handler
   struct termios info;
   tcgetattr(0, &info);          /* get current terminal attirbutes; 0 is the file descriptor for stdin */
   info.c_lflag &= ~ICANON;      /* disable canonical mode */
   info.c_cc[VMIN] = 1;          /* wait until at least one keystroke available */
   info.c_cc[VTIME] = 0;         /* no timeout */
   tcsetattr(0, TCSANOW, &info); /* set immediately */

线程调用的内部函数(抱歉缩进):

while(stop_wait != 1) 
      {
         //printf("%d\n", temp->currentID);
         ch = getchar();

         if(ch < 0) {
            if (ferror(stdin)) {
               clearerr(stdin);
            }
         }

         switch (ch)
         {
         case 48 :
            if(temp->event == 0) stop_wait = 1;
            break;
         case 49 :
            if(temp->event == 1) stop_wait = 1;
            break;
         case 50 :
            if(temp->event == 2) stop_wait = 1;
            break;
         case 51 :
            if(temp->event == 3) stop_wait = 1;
            break;
         case 52 :
            if(temp->event == 4) stop_wait = 1;
            break;
         }
      }

主线结束:

tcgetattr(0, &info);
info.c_lflag |= ICANON;
tcsetattr(0, TCSANOW, &info);

上面的代码与此处的代码非常相似:Implementing a KeyPress Event in C

然而,这并没有按照我想要的方式工作。我有一个输入文件,指定哪些键将触发 stop_wait 更改为 1。线程 1 将通过按键盘上的 1(ascii 中的 49)触发,线程 2 将通过按键盘上的 2 触发(ascii 格式为 50)。当前实现的问题是 2 不会在没有触发 1 的情况下触发。如下图(Main()语句显示执行结束忽略它说的是什么):

对于这个问题,我可以获得任何建议/帮助吗?

我在评论中提到的 multi-threaded 方法,它有一个单独的线程来获取和排队键,被设计为不丢弃键,这不是微不足道的。它需要一些 C 技能和一些 UNIX 知识。我实现了一个 运行s 的工作框架,因此您可以看到其中涉及的内容。

要对此进行测试,请将文件另存为 dispatch.c

$ cc -o dispatch dispatch.c
$ ./dispatch

示例输出:

$ ./dispatch
Key 'a' pressed...
... Thread T3 pulled key 'a' from queue
... Thread T1 pulled key 'a' from queue
... Thread T2 pulled key 'a' from queue
Key 'b' pressed...
... Thread T2 pulled key 'b' from queue
... Thread T1 pulled key 'b' from queue
Key 'c' pressed...
... Thread T3 pulled key 'c' from queue
... Thread T1 pulled key 'c' from queue
Key 'd' pressed...
... Thread T2 pulled key 'd' from queue
... Thread T3 pulled key 'd' from queue
Key 'z' pressed...
... Thread T2 pulled key 'z' from queue
... Thread T1 pulled key 'z' from queue
... Thread T3 pulled key 'z' from queue

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <strings.h>
#include <string.h>
#include <termios.h>
#include <sys/types.h>

typedef struct keyQueue {
    struct keyQueue *next;
    char key;
} keyQueue_t;

typedef struct ThreadInfo {
    pthread_t tid;           /* thread id */
    pthread_mutex_t kqmutex; /* protects key queue from race condition between threads */
    keyQueue_t kqhead;       /* input keys queued to this thread */
    char *keys;              /* keys this thread responds to */
    char *name;              /* name of this thread */
} threadInfo_t;

static struct termios origtc, newtc;

threadInfo_t threads[] = { 
    { 0, PTHREAD_MUTEX_INITIALIZER, { NULL, '[=11=]' }, "abcez", "Thread T1" },
    { 0, PTHREAD_MUTEX_INITIALIZER, { NULL, '[=11=]' }, "abdfz", "Thread T2" },
    { 0, PTHREAD_MUTEX_INITIALIZER, { NULL, '[=11=]' }, "acdgz", "Thread T3" }
};

void *service(void *arg) {
    char key;
    threadInfo_t *t = &threads[(int)arg];    // get pointer to thread
    for(;;) {
        pthread_mutex_lock(&t->kqmutex);     // lock other threads out while we tamper 
        key = '[=11=]';                          // initialize key to NULL
        if (t->kqhead.next != NULL) {        // Anything queued up for us?
            keyQueue_t *kq = t->kqhead.next; // if so get ptr to key pkt
            key = kq->key;                   // fetch key from pkt
            t->kqhead.next = kq->next;       // Point to next key in queue (or NULL if no more queued up).
            free(kq);
        }  
        pthread_mutex_unlock(&t->kqmutex);   // unlock key queue
        if (key != '[=11=]') {                   // if we got a key, log it
            printf("... %s pulled key '%c' from queue\n", t->name, key);
        }
        // ⇓ usleep() probably more practical as 1-sec too long for most cases
        sleep(1);                            // sleep so we don't loop too fast eating CPU
    }
    return NULL;
}

int main() {

    /* Fire up threads */
    for (long i = 0; i < sizeof (threads) / sizeof (threadInfo_t); i++) {
        if (pthread_create(&threads[i].tid, NULL, service, (void *)i) < 0) {
            perror("pthread_create()");
            exit(-1);
        }
    }

    tcgetattr(0, &origtc);                         // get orig tty settings
    newtc = origtc;                                // copy them
    newtc.c_lflag &= ~ICANON;                      // put in '1 key mode'
    newtc.c_lflag &= ~ECHO;                        // turn off echo

    for(;;) {
        tcsetattr(0, TCSANOW, &newtc);             // echo off 1-key read mode
        char c = getchar();                        // get single key immed.
        tcsetattr(0, TCSANOW, &origtc);            // settings back to normal
        printf("Key '%c' pressed...\n", c);        // show user what we got
        for (int i = 0; i < sizeof (threads) / sizeof (threadInfo_t); i++) {
            threadInfo_t *t = &threads[i];         // get shorthand ptr to thread
            if (strchr(t->keys, c) != NULL) {      // this thread listens for this key
                pthread_mutex_lock(&t->kqmutex);   // lock other threads out while we tamper 
                keyQueue_t *kq = calloc(sizeof (struct keyQueue), 1); // allocate pkt
                kq->key = c;                       // stash key there
                keyQueue_t *kptr = &t->kqhead;     // get pointer to queue head
                while(kptr->next != NULL)          // find first empty slot
                    kptr = kptr->next;
                kptr->next = kq;                   // enqueue key packet to thread
                pthread_mutex_unlock(&t->kqmutex); // unlock key queue
            }
        }
    }
}

此代码启动三个线程,t1、t2、t3,每个线程都有一个 'key queue' 结构,以及一个 char * 字段 keyskeys 是一个字符串,其中包含线程是 'interested in' 的字符(键)。

字符串中列出的键盘键在线程字符串中重复,因此在某些情况下一个键可以被多个线程使用。例如,所有线程监听'a'和'z',两个线程监听'b',另外两个线程监听'c',另外一对线程监听'd',最后 'e'、'f' 和 'g' 分别只有一个线程监听。

主循环读取键而不回显并立即捕获键(例如,用户不必点击 return)。当输入一个键时,它会循环遍历线程信息以找出哪些线程对按下的键感兴趣,并将键(在数据包中)排队到相应的线程。

线程在它们自己的循环中,在循环之间休眠一秒钟。当他们醒来时,他们检查他们的队列以查看是否有任何钥匙在排队。如果有,他们将其从队列中拉出并说他们从队列中拉出该密钥。

由于每个线程的 polling/work 循环延迟(例如,在线程唤醒并检查各自的队列之前),您有时间在键盘上输入多个内容以排队到线程,然后线程将以 1 秒的间隔一次一个地使入队键出队。

在现实生活中,该程序会使用更短的睡眠时间,但会在其中放置 某些内容 以防止每个线程不必要地占用大量 CPU 时间。

运行 很有趣,看看它的实际效果。

*注意:使用calloc()而不是malloc(),因为与malloc()不同,calloc()将内存returned初始化为全0。这是一个很好的技巧。