[Python-ideas] Simpler thread synchronization using "Sticky Condition"

Richard Whitehead richard.whitehead at ieee.org
Tue Mar 26 05:27:18 EDT 2019


Problem: 

Using Python's Condition class is error-prone and difficult. For example,
the following pseudo-code would be quite typical of its use:

 

condition = threading.Condition()

 

def sender():

     while alive():

          wait_for_my_data_from_hardware()

           with condition:

                send_data_to_receiver()

                condition.raise()

 

def receiver():

     while alive():

          with condition:

                 condition.wait()

                 receive_all_data_from_sender()

                 process_data()

 

Main code will then run sender() and receiver() in separate threads.

 

(I know that in a simple case like this, I could just use a Queue. I've been
working with code that needs to service several events, where polling queues
would introduce overhead and latency so a Condition must be used, but I've
simplified the above as much as possible to illustrate the issues even in
this simple case).

 

There are two issues even with the above case:

1. Raising a condition only has any effect if the condition is already being
waited upon; a condition does not "remember" that it has been raised. So in
the example above, if the receiver starts after the sender, it will wait on
the condition even though data is already available for it.  This issue can
be solved by rearranging the code, waiting at the end of the loop rather
than the start; but this is counter-intuitive and lots of examples online
look like the above.

2. In the receiver, the condition has to be kept locked (be inside the
"with" statement") all the time. The lock is automatically released while
the condition is being waited upon, but otherwise it must be locked,
otherwise we risk missing the condition being raised. The result is that
process_data() is called with the lock held - and so this will prevent the
sender going round its loop. The sending thread becomes a slave to the
processing thread. This may have a huge performance penalty, losing the
advantage of loose coupling that threading should provide.

 

You might think that using an Event, rather than a Condition, would solve
things, since an Event does "remember" that is has been set. But there is no
atomic way to wait on an event and clear it afterwards.

 

Solution:

The solution is very simple: to create something that might be called a
StickyCondition, or an AutoResetEvent. This is a Condition that does
remember it has been raised; or if you prefer, it is an Event that resets
itself after it has been waited upon.  The example then becomes:

 

auto_event = threading.AutoResetEvent()

 

def sender():

     while alive():

           wait_for_my_data_from_hardware()

           send_data_to_receiver()

           auto_event.set()

 

def receiver():

     while alive():

           auto_event.wait()

           receive_all_data_from_sender()

           process_data()

 

This solves both of the issues described: the receiver will not wait if data
is already available, and the sender is not blocked by a lock that is being
held by the receiver. The event doesn't need to be locked at all, because of
its internal memory.

 

Implementation is trivial, involving a boolean and a Condition in the
cross-thread case. I presume it would be more involved for the cross-process
case, but I can see no reason it would be impossible.

 

Please let me know if you think this would be useful.

 

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20190326/e1d826d2/attachment.html>


More information about the Python-ideas mailing list