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

前端 未结 8 1627
悲&欢浪女
悲&欢浪女 2020-12-09 03:35

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

相关标签:
8条回答
  • 2020-12-09 03:47

    Extending Claudiu's answer where you can either wait for:

    1. event 1 OR event 2
    2. event 1 AND even 2

    from threading import Thread, Event, _Event
    
    class ConditionalEvent(_Event):
        def __init__(self, events_list, condition):
            _Event.__init__(self)
    
            self.event_list = events_list
            self.condition = condition
    
            for e in events_list:
                self._setup(e, self._state_changed)
    
            self._state_changed()
    
        def _state_changed(self):
            bools = [e.is_set() for e in self.event_list]
            if self.condition == 'or':                
                if any(bools):
                    self.set()
                else:
                    self.clear()
    
            elif self.condition == 'and':                 
                if all(bools):
                    self.set()
                else:
                    self.clear()
    
        def _custom_set(self,e):
            e._set()
            e._state_changed()
    
        def _custom_clear(self,e):
            e._clear()
            e._state_changed()
    
        def _setup(self, e, changed_callback):
            e._set = e.set
            e._clear = e.clear
            e._state_changed = changed_callback
            e.set = lambda: self._custom_set(e)
            e.clear = lambda: self._custom_clear(e)
    

    Example usage will be very similar as before

    import time
    
    e1 = Event()
    e2 = Event()
    
    # Example to wait for triggering of event 1 OR event 2
    or_e = ConditionalEvent([e1, e2], 'or')
    
    # Example to wait for triggering of event 1 AND event 2
    and_e = ConditionalEvent([e1, e2], 'and')
    
    0 讨论(0)
  • 2020-12-09 03:50

    This is an old question, but I hope this helps someone coming from Google.
    The accepted answer is fairly old and will cause an infinite loop for twice-"orified" events.

    Here is an implementation using concurrent.futures

    import concurrent.futures
    from concurrent.futures import ThreadPoolExecutor
    
    def wait_for_either(events, timeout=None, t_pool=None):
        '''blocks untils one of the events gets set
    
        PARAMETERS
        events (list): list of threading.Event objects
        timeout (float): timeout for events (used for polling)
        t_pool (concurrent.futures.ThreadPoolExecutor): optional
        '''
    
        if any(event.is_set() for event in events):
            # sanity check
            pass
        else:
            t_pool = t_pool or ThreadPoolExecutor(max_workers=len(events))
            tasks = []
            for event in events:
                tasks.append(t_pool.submit(event.wait))
    
            concurrent.futures.wait(tasks, timeout=timeout, return_when='FIRST_COMPLETED')
            # cleanup
            for task in tasks:
                try:
                    task.result(timeout=0)
                except concurrent.futures.TimeoutError:
                    pass
    

    Testing the function

    import threading
    import time
    from datetime import datetime, timedelta
    
    def bomb(myevent, sleep_s):
        '''set event after sleep_s seconds'''
        with lock:
            print('explodes in ', datetime.now() + timedelta(seconds=sleep_s))
        time.sleep(sleep_s)
        myevent.set()
        with lock:
            print('BOOM!')
    
    lock = threading.RLock()  # so prints don't get jumbled
    a = threading.Event()
    b = threading.Event()
    
    t_pool = ThreadPoolExecutor(max_workers=2)
    
    threading.Thread(target=bomb, args=(event1, 5), daemon=True).start()
    threading.Thread(target=bomb, args=(event2, 120), daemon=True).start()
    
    with lock:
        print('1 second timeout, no ThreadPool', datetime.now())
    
    wait_for_either([a, b], timeout=1)
    
    with lock:
        print('wait_event_or done', datetime.now())
        print('=' * 15)
    
    with lock:
        print('wait for event1', datetime.now())
    
    wait_for_either([a, b], t_pool=t_pool)
    
    with lock:
        print('wait_event_or done', datetime.now())
    
    0 讨论(0)
  • 2020-12-09 03:51

    Starting extra threads seems a clear solution, not very effecient though. Function wait_events will block util any one of events is set.

    def wait_events(*events):
        event_share = Event()
    
        def set_event_share(event):
            event.wait()
            event.clear()
            event_share.set()
        for event in events:
            Thread(target=set_event_share(event)).start()
    
        event_share.wait()
    
    wait_events(event1, event2, event3)
    
    0 讨论(0)
  • 2020-12-09 03:59

    I think the standard library provides a pretty canonical solution to this problem that I don't see brought up in this question: condition variables. You have your main thread wait on a condition variable, and poll the set of events each time it is notified. It is only notified when one of the events is updated, so there is no wasteful polling. Here is a Python 3 example:

    from threading import Thread, Event, Condition
    from time import sleep
    from random import random
    
    event1 = Event()
    event2 = Event()
    cond = Condition()
    
    def thread_func(event, i):
        delay = random()
        print("Thread {} sleeping for {}s".format(i, delay))
        sleep(delay)
    
        event.set()
        with cond:
            cond.notify()
    
        print("Thread {} done".format(i))
    
    with cond:
        Thread(target=thread_func, args=(event1, 1)).start()
        Thread(target=thread_func, args=(event2, 2)).start()
        print("Threads started")
    
        while not (event1.is_set() or event2.is_set()):
            print("Entering cond.wait")
            cond.wait()
            print("Exited cond.wait ({}, {})".format(event1.is_set(), event2.is_set()))
    
        print("Main thread done")
    

    Example output:

    Thread 1 sleeping for 0.31569427100177794s
    Thread 2 sleeping for 0.486548134317051s
    Threads started
    Entering cond.wait
    Thread 1 done
    Exited cond.wait (True, False)
    Main thread done
    Thread 2 done
    

    Note that wit no extra threads or unnecessary polling, you can wait for an arbitrary predicate to become true (e.g. for any particular subset of the events to be set). There's also a wait_for wrapper for the while (pred): cond.wait() pattern, which can make your code a bit easier to read.

    0 讨论(0)
  • 2020-12-09 04:03

    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()
    
    0 讨论(0)
  • 2020-12-09 04:03

    Not pretty, but you can use two additional threads to multiplex the events...

    def wait_for_either(a, b):
      flag = False #some condition variable, event, or similar
    
      class Event_Waiter(threading.Thread):
        def __init__(self, event):
            self.e = event
        def run(self):
            self.e.wait()
            flag.set()
    
      a_thread = Event_Waiter(a)
      b_thread = Event_Waiter(b)
      a.start()
      b.start()
      flag.wait()
    

    Note, you may have to worry about accidentally getting both events if they arrive too quickly. The helper threads (a_thread and b_thread) should lock synchronize around trying to set flag and then should kill the other thread (possibly resetting that thread's event if it was consumed).

    0 讨论(0)
提交回复
热议问题