Search code examples
pythonmultithreadingncursescurses

Preventing race condition with ncurses+python


I have a problem similar to the one in this post

C, junk appearing on screen using curses

However, I am using python. I wrote a wrapper Screen class that wraps the curses module in a more convenient way. I have a getch like this

def getKeyCode(self):                                                                                                                         
    with self._curses_lock:                                                                                                                   
        self._curses_screen.nodelay(False)                                                                                                    

    c = self._curses_screen.getch()                                                                                                           
    if c == 27:                                                                                                                               
        with self._curses_lock:                                                                                                               
            self._curses_screen.nodelay(True)                                                                                                 
        next_c = self._curses_screen.getch()                                                                                                  

        with self._curses_lock:                                                                                                               
            self._curses_screen.nodelay(False)                                                                                                

    return c          

and a write which boils down to this

        with self._curses_lock:                                                                                                               
            self._curses_screen.addstr(y, x, out_string, attr)

The getKeyCode is called by a separate python thread that waits in the getch for most of the time, and when a key is pressed, it gets added to a Queue. On the other (main) thread, I have an event loop that gets the events from the queue, performs repaints and refresh the screen.

As I know that ncurses holds shared state, I added a bunch of threading.Locks in order to prevent race conditions, yet if I keep pressed the arrow key, occasionally I get garbage. I guess that this is due to getch getting freed inside ncurses while a repainting is going on from the other thread. This messes up the state of the repaint, giving weird results. I obviously can't have my getKeyCode thread to hold the lock, because that would mean to prevent repainting at all until the getch returns, nor I want to have a getch non-blocking, since I would 1) not really solve the problem and 2) have a threading constantly running that would pump up the CPU usage to 100%. How can I solve this problem?


Solution

  • I solved the problem, like this. I set getch nonblocking with

    self._curses_screen.nodelay(True) 
    

    then I do the wait not inside curses, but with a select on stdin. When the select returns, some stuff is available and I can lock and get exclusive access to the ncurses backend, guaranteed that I will return immediately with what's available and release the lock.

    def getKeyCode(self):                                                                                                                         
        select.select([sys.stdin], [], [])                                                                                                        
    
        with self._curses_lock:                                                                                                                   
    
            c = self._curses_screen.getch()                                                                                                       
            if c == 27:                                                                                                                           
                next_c = self._curses_screen.getch()                                                                                              
    
        return c