cmultithreadingncursestermiosconio

Implementing a KeyPress Event in C with Multiple Threads


My goal: A thread will wait (busy loop not sleep) until a specific key (lets say 0) is pressed. Each thread has a different key that will trigger that thread to get out of waiting and progress through the commands that follow the wait.

I have tried the following to implement this:

using conio.h and getch() but this is old and doesnt work with gcc anymore. Source: Why can't I find <conio.h> on Linux?

using ncurses.h and getch() but this stops execution while waiting for a keyboard press. Code I used: http://tldp.org/HOWTO/NCURSES-Programming-HOWTO/scanw.html#GETCHCLASS

My current implementation using termios.h:

int main:

      //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 */

Inside function called by thread (sorry about the indentation):

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

End of main:

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

The code above is very similar to what is found here: Implementing a KeyPress Event in C

However this doesn't work the correct way I want it to. I have an input file specifying which keys will trigger the stop_wait to be changed to 1. Thread 1 will be triggered by pressing 1 on the keyboard (49 in ascii) and Thread 2 will be triggered by pressing 2 on the keyboard (50 in ascii). The problem with the current implementation is that 2 will not trigger without 1 being triggered. As shown below (the Main() statement shows the end of execution ignore what it says): enter image description here

Can I get any advice / help with this issue?


Solution

  • The multi-threaded approach I mentioned in the comments, that has a separate thread to fetch and enqueue keys, being designed not to drop keys, isn't trivial. It requires some C skills and some UNIX knowledge. I implemented a working skeleton that runs, so you can see what's involved.

    To test this, save the file as, let's say, dispatch.c

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

    Sample output:

    $ ./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, '\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
                }
            }
        }
    }
    

    This code starts three threads, t1, t2, t3, which each have a 'key queue' structure on them, as well as a char * field keys. keys is a string containing the characters (keys) the thread is 'interested in'.

    The keyboard keys listed in the string are duplicated in the threads string so that one key can be consumed by more than one thread in some cases. For example, all the threads listen to 'a' and 'z', two threads listen to 'b', another two to 'c', another pair of threads are interested in 'd', and finally 'e', 'f', and 'g' have only one thread listening, respectively.

    The main loop reads keys without echo and captures keys immediately (e.g. without the user having to hit return). When a key is entered, it loops through the thread info to find out which threads are interested in the pressed key and enqueues the key (in a packet) to the respective thread(s).

    The threads are in their own loop, sleeping one second in between loops. When they wake up they check their queue to see if there are any keys queued. If there are they pull it from the queue and say they pulled that key from the queue.

    Because of the delay in each thread's polling/work loop (e.g. before the threads wake up and check their respective queues), there's time for you to enter multiple things on the keyboard to get queued up to the threads, and then the threads will dequeue them the enqueue keys one at a time at 1 second intervals.

    In real life the program would use a much shorter sleep, but would put something in there to keep each thread from needlessly hogging a lot of CPU time.

    Kind of fun to run it and see it in action.

    *Note: calloc() is used instead of malloc() because unlike malloc(), calloc() initializes the memory returned to all 0's. It's a nice trick.