Skip to content

Commit

Permalink
Updating thread-safe-attributes example.
Browse files Browse the repository at this point in the history
Ran the writing through a grammar checker, added links, changed some of
the clumsy language.  Changed the order of the example to be 3rd from
the top because it is a basic feature.
  • Loading branch information
aleph2c committed Sep 19, 2019
1 parent f84cea5 commit d3d4f7b
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 106 deletions.
2 changes: 1 addition & 1 deletion doc/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ Examples
:caption: Single Chart Examples:

comprehensive
thread_safe_attributes
scribbleexample
interactingcharts
towardsthefactoryexample
thread_safe_attributes
cellular_automata
i_mongol_example

Expand Down
101 changes: 67 additions & 34 deletions doc/thread_safe_attributes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Seems simple enough. Suppose you pick a straight-forward strategy:

But while calculated the ``a*cos(0.45)`` part of the problem, someone grabs your
paper, changes your temporary value of ``a`` to ``0.3``, then puts it back on
your desk. You don't notice. When you get to the ``3*a^1.2`` part of the
your desk. You don't notice it. When you get to the ``3*a^1.2`` part of the
calculation, you use the wrong ``a`` value, so you get the wrong answer for ``b``.

This is called a **race condition**. Here our ``a`` variable was shared between
Expand All @@ -38,43 +38,43 @@ A simple way to avoid such a situation is to not share the temporary paper in
the first place. Do not use shared attributes.

Another way to deal with it is to have one thread change a shared attribute and
have the other thread read the shared attribute. But, this will required that
maintenance developers understand there are hidden rules in your code base;
have the other thread read the shared attribute. But, this will require that
maintenance developers understand there are hidden rules in your codebase;
they could innocently change something an introduce extremely subtle bugs.

Typically, shared variables are protected using thread locks. A lock is a flag
which works across multiple threads. You can lock the object for read/writing
while you use it. In our example, we would lock ``a`` in its ``0.35`` state
while calculating both sub-parts of our problem then unlock it when we are done.
The other process, would simply wait until the thread-lock cleared, then they
would change the value of ``a`` to ``0.3`` and do their own work. So, there is
a cost, you block one thread while waiting for the other to work, and you have
to share lock variables across all of your threads. It is easy to screw this
up, and it is very hard to test for race conditions.
which works across multiple threads. You can lock the object for reading and
writing while you use it. In our example, we would lock ``a`` in its ``0.35``
state while calculating both sub-parts of our problem then unlock it when we are
done. The other process would wait until the thread-lock cleared, then
they would change the value of ``a`` to ``0.3`` and do their own work. So,
there is a cost, you block one thread while waiting for the other to work, and
you have to share lock variables across all of your threads. It is easy to
screw this up, and it is tough to test for race conditions.

But why is it hard to test for race conditions? As of Python 3, a thread will
run for 15 milliseconds before Python passes control to another thread. Most of
the time, the common memory that is used by both threads will work as you expect
it to. Infrequently a thread switch will occur midway through a non-atomic
it will. Infrequently a thread switch will occur midway through a non-atomic
operation, where some shared value is to be changed by the other
thread. After this unlikely event, your thread will re-gain control and finish
its calculation producing the wrong answer.

These kinds of bugs are more probabilistic than deterministic, because Python's
access to the system clock is jittery. It's timing will never be the same for
every two runs of the program (it's like playing a slot machine) so it will be
hard for you to reproduce your issue.
These kinds of bugs are more probabilistic in nature, than deterministic;
Python's access to the system clock is jittery. The timing between two Python
threads will never be the same for every two runs of the program (it's like
playing a slot machine) so, it will be hard for you to reproduce your issue.

The miros library accepts that people will want to access a statechart's
internal attributes from the outside. Significant efforts have been made to
make this kind of activity easy for you to do in a "thread-safe" manner. The
``ThreadSafeAttributes`` class was constructed to eat the complexity of making
thread safe attributes by wrapping "getting" (use of the ".") and "setting"
operations (use of the "=") within thread safe locks. In addition to this, the
thread-safe attributes by wrapping "getting" (use of the ".") and "setting"
operations (use of the "=") within thread-safe locks. In addition to this, the
non-atomic "+=", "-=" ... "//=" statements using thread-safe attributes were
also wrapped within locks. For situations that are more complex, the
also wrapped within locks. For more complex situations, the
thread-safety features provided by the ``ThreadSafeAttributes`` class can be
used to explicitly get the thread lock.
used get to get the thread lock explicitly.

I will introduce these ideas gradually through a set of examples. Let's
begin by looking at four interacting threads (possible race conditions are
Expand Down Expand Up @@ -148,6 +148,11 @@ highlighted):
thread_stopper.start()
thread_stopper.join()
.. note::

You can download the above code `here
<https://github.com/aleph2c/miros/blob/master/examples/thread_safe_attributes_1.py>`_

The ``GetLock1`` class inherits from the ``ThreadSafeAttributes`` class, which
uses a metaclass to give it access to the following syntax (seen on line 8 of
the above example):
Expand All @@ -157,7 +162,7 @@ the above example):
_attributes = ['thread_safe_attr_1']
The ``ThreadSafeAttributes`` class tries to protect you. When we write the
``_attributes = ['thread_safe_attr_1']`` syntax ``ThreadSafeAttributes`` creates
``_attributes = ['thread_safe_attr_1']`` syntax, ``ThreadSafeAttributes`` creates
a set of hidden attributes, which are wrapped inside of a `descriptor protocol
<https://docs.python.org/3.6/howto/descriptor.html>`_ (think @property). One of
the hidden attributes, `_lock` is a `threading.RLock
Expand All @@ -180,9 +185,18 @@ used to lock and unlock itself around accesses to the other hidden attribute
with gl1._lock:
gl1.thread_safe_attr_1 = 1
This is a typical meta-programming feature a lot of libraries provide. But
things get a bit weird when we look at the non-atomic ``+=`` in the following
code example:
.. note::

A lot of Python libraries provide features to change simple syntax into more
complex and specific syntax prior to having it run. If this library was
written in c, this kind of work would be done inside of a macro, and the
preprocessor would create custom c-code before it was compiled into an
executable.

The ``ThreadSafeAttributes`` class also tries to protect your code from race
conditions introduced by non-atomic ``+=`` statements acting on shared
attributes:

.. code-block:: python
Expand Down Expand Up @@ -277,6 +291,11 @@ highlighted):
thread_stopper.start()
thread_stopper.join()
.. note::

You can download the above code `here
<https://github.com/aleph2c/miros/blob/master/examples/thread_safe_attributes_2.py>`_

We haven't looked at any code results yet. Let's run it and see what it does:

.. code-block:: bash
Expand All @@ -294,12 +313,13 @@ We haven't looked at any code results yet. Let's run it and see what it does:
th2: 0
We see that the number oscillates about 0. If we remove the time delays at the
bottom of the thread functions you will see wild oscillation in this number,
since one thread by change will get many more opportunities to run. So you can
see that it might be hard to reproduce exactly two identical traces of the
bottom of the thread functions, you will see wild oscillation in this number,
since one thread by chance will get many more opportunities to run. So you can
see that it might be hard to reproduce precisely two identical traces of the
program output.

Ok, now for something scary, let's look at our code without thread locks (the race conditions are highlighted):
Ok, now for something scary, let's look at our code without thread-locks (the
race conditions are highlighted):

.. code-block:: python
:emphasize-lines: 15, 16, 28, 29
Expand Down Expand Up @@ -365,6 +385,11 @@ Ok, now for something scary, let's look at our code without thread locks (the ra
thread_stopper.start()
thread_stopper.join()
.. note::

You can download the above code `here
<https://github.com/aleph2c/miros/blob/master/examples/thread_safe_attributes_3_unsafe.py>`_

I changed the ``thread_safe_attr_1`` name to ``thread_race_attr_1`` to make a
point. The highlighted code shows where race conditions can occur. If we run
the code we see:
Expand All @@ -386,7 +411,7 @@ the code we see:
Which looks almost exactly the same as the last run. Race conditions are very
hard to find.

Let's move back to our original-original example, suppose we absolutely needed
Let's move back to our original example, suppose we absolutely needed
to run calculations on the ``thread_safe_attr_1`` in more than one thread (which
I can't see the need for). I'll change the name of ``thread_safe_attr_1`` to
``a``. The ``ThreadSafeAttributes`` class can not implicitly protect you in such
Expand Down Expand Up @@ -468,16 +493,23 @@ protect your own code (highlighting how to get the lock):
thread_stopper.start()
thread_stopper.join()
.. note::

You can download the above code `here
<https://github.com/aleph2c/miros/blob/master/examples/thread_safe_attributes_4.py>`_

The lock can be obtained by calling ``_, _lock = <thread_safe_attribute>``.

This is a little nasty piece of metaprogramming that could baffle a beginner or
anyone who looks at the thread safe attribute. Most of the time your thread
safe attribute acts as an attribute, but other times it acts as an iterable,
what gives? It only acts as an interable when proceeded by ``_, _lock``. If
you use this technique in one of your threads, you must use it in all of your
threads. Once again I recommend against performing calculations directly on
your shared attributes. Instead, copy their variable into a temp, perform a
calculation then assign the results into them.
what is going on? It only acts as an interable when proceeded by ``_, _lock``.
If you use this technique in one of your threads, you must use it in all of your
threads.

Once again I recommend against performing calculations directly on your shared
attributes. Instead, copy their variable into a temp, perform a calculation
then assign the results into them.

.. note::

Expand All @@ -486,3 +518,4 @@ calculation then assign the results into them.
is because of this feature it can release it's lock in what looks like a
syntactically inconsistent way.

:ref:`back to examples <examples>`
2 changes: 1 addition & 1 deletion docs/_sources/examples.rst.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ Examples
:caption: Single Chart Examples:

comprehensive
thread_safe_attributes
scribbleexample
interactingcharts
towardsthefactoryexample
thread_safe_attributes
cellular_automata
i_mongol_example

Expand Down
Loading

0 comments on commit d3d4f7b

Please sign in to comment.