【问题标题】:Implementing a KeyPress Event in C with Multiple Threads用多线程在 C 中实现 KeyPress 事件
【发布时间】:2017-01-30 03:35:58
【问题描述】:

我的目标:线程将等待(忙循环而不是睡眠),直到按下特定键(比如说 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 的实现:

int 主要:

      //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 )。当前实现的问题是,如果不触发 1,则 2 不会触发。如下所示(Main() 语句显示执行结束忽略它所说的内容):

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

【问题讨论】:

  • ' free running' 线程可以轮询状态以检测按键被按下并查看哪个按键并做出相应响应?
  • 很多方案来决定如何对其进行微调。例如,没有键被丢弃。甚至每个线程都可能有一个fifo,并且“kbd inpt/dispatch”线程可以将消息排队到它需要发出信号的适当线程。在该方案中(无论您如何实现先进先出),您都可以确保您永远不会错过任何击键。例如,如果线程 1 需要为每次按下“a”做某事,它就永远不会错过按键
  • 我没有那样看。谢谢!我会尝试实现它并看看,但我认为这肯定会奏效。
  • 这可能不是您的情况所需要的,但我确实创建了一个工作示例(答案如下)。您可以调整轮询间隔(使用小于 1 部分的等待,而不是 sleep(),使用 usleep())。你可能会弄乱排队策略。但值得测试 IMO 代码。
  • 感谢您的关注。我刚刚更新了答案。我忘了 free() 分配的内存,所以我刚刚添加了那行。否则会泄漏内存。

标签: c multithreading ncurses termios conio


【解决方案1】:

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

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

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

样本输出:

$ ./dispatch
按下键“a”...
... 线程 T3 从队列中拉出键“a”
... 线程 T1 从队列中拉出键“a”
... 线程 T2 从队列中拉出键“a”
按下键“b”...
... 线程 T2 从队列中拉出键“b”
... 线程 T1 从队列中拉出键“b”
按下“c”键...
... 线程 T3 从队列中拉出键“c”
... 线程 T1 从队列中拉出键“c”
按下“d”键...
... 线程 T2 从队列中拉出键“d”
... 线程 T3 从队列中拉出键“d”
按下“z”键...
... 线程 T2 从队列中拉出键“z”
... 线程 T1 从队列中拉出键“z”
... 线程 T3 从队列中提取了键“z”

#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, '\0' }, "abcez", "Thread T1" },
    { 0, PTHREAD_MUTEX_INITIALIZER, { NULL, '\0' }, "abdfz", "Thread T2" },
    { 0, PTHREAD_MUTEX_INITIALIZER, { NULL, '\0' }, "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 = '\0';                          // 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 != '\0') {                   // 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,每个线程上都有一个“键队列”结构,以及一个char * 字段keyskeys 是一个字符串,其中包含线程“感兴趣”的字符(键)。

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

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

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

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

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

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

*注意:calloc() 用于代替malloc(),因为与malloc() 不同,calloc() 将返回的内存初始化为全0。这是一个不错的技巧。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多