Search code examples
pythonmultithreading

Python threading: can I sleep on two threading.Event()s simultaneously?


If I have two threading.Event() objects, and wish to sleep until either one of them is set, is there an efficient way to do that in python? Clearly I could do something with polling/timeouts, but I would like to really have the thread sleep until one is set, akin to how select is used for file descriptors.

So in the following implementation, what would an efficient non-polling implementation of wait_for_either look like?

a = threading.Event()
b = threading.Event()

wait_for_either(a, b)

Solution

  • Here is a non-polling non-excessive thread solution: modify the existing Events to fire a callback whenever they change, and handle setting a new event in that callback:

    import threading
    
    def or_set(self):
        self._set()
        self.changed()
    
    def or_clear(self):
        self._clear()
        self.changed()
    
    def orify(e, changed_callback):
        e._set = e.set
        e._clear = e.clear
        e.changed = changed_callback
        e.set = lambda: or_set(e)
        e.clear = lambda: or_clear(e)
    
    def OrEvent(*events):
        or_event = threading.Event()
        def changed():
            bools = [e.is_set() for e in events]
            if any(bools):
                or_event.set()
            else:
                or_event.clear()
        for e in events:
            orify(e, changed)
        changed()
        return or_event
    

    Sample usage:

    def wait_on(name, e):
        print "Waiting on %s..." % (name,)
        e.wait()
        print "%s fired!" % (name,)
    
    def test():
        import time
    
        e1 = threading.Event()
        e2 = threading.Event()
    
        or_e = OrEvent(e1, e2)
    
        threading.Thread(target=wait_on, args=('e1', e1)).start()
        time.sleep(0.05)
        threading.Thread(target=wait_on, args=('e2', e2)).start()
        time.sleep(0.05)
        threading.Thread(target=wait_on, args=('or_e', or_e)).start()
        time.sleep(0.05)
    
        print "Firing e1 in 2 seconds..."
        time.sleep(2)
        e1.set()
        time.sleep(0.05)
    
        print "Firing e2 in 2 seconds..."
        time.sleep(2)
        e2.set()
        time.sleep(0.05)
    

    The result of which was:

    Waiting on e1...
    Waiting on e2...
    Waiting on or_e...
    Firing e1 in 2 seconds...
    e1 fired!or_e fired!
    
    Firing e2 in 2 seconds...
    e2 fired!
    

    This should be thread-safe. Any comments are welcome.

    EDIT: Oh and here is your wait_for_either function, though the way I wrote the code, it's best to make and pass around an or_event. Note that the or_event shouldn't be set or cleared manually.

    def wait_for_either(e1, e2):
        OrEvent(e1, e2).wait()