Locks and Synchronization¶
Although parallelism cannot be obtained with Python, its multithreading works well for simulating concurrent events.
Threads for Simulation¶
In our simulation example, consider customers arriving at a fastfood restaurant, who wait for their order to be ready; and then spend some time eating and then leave.
In our model, customers are represented by threads.
Inheriting from the class Thread
,
the data object attributes are:
- The name of the customer; and
- the waiting and eating time.
Names are entered when threads are born.
Waiting and eating times are generated via
random.randint()
in the __init__
.
The simulation may run as below.
$ python fastfood.py
give #customers : 3
name of customer 0 : A
name of customer 1 : B
name of customer 2 : C
starting the simulation
A is waiting for 1 time units
B is waiting for 1 time units
C is waiting for 4 time units
simulation has started
A waited 1 time units
B waited 1 time units
B ate for 2 time units
C waited 4 time units
A ate for 6 time units
C ate for 5 time units
$
The script fastfood.py
has the following documentation strings:
from threading import Thread
class Customer(Thread):
"""
Customer orders food and eats.
"""
def __init__(self, t):
"""
Initializes customer with name t,
generates waiting and eating time.
"""
def run(self):
"""
Customer waits for order and eats.
writes three messages.
"""
def main():
"""
Defines and starts threads.
"""
The main()
in fastfood.py
is defined below.
def main():
"""
Defines and starts threads.
"""
nbr = int(input('give #customers : '))
threads = []
for i in range(nbr):
prompt = 'name of customer %d : ' % i
name = input(' ' + prompt)
threads.append(Customer(name))
print("starting the simulation")
for customer in threads:
customer.start()
print("simulation has started")
Code for the constructor in the customer is below.
from random import randint
class Customer(Thread):
"""
Customer orders food and eats.
"""
def __init__(self, t):
"""
Initializes customer with name t,
generates waiting and eating time.
"""
Thread.__init__(self, name=t)
self.wait = randint(1, 6)
self.eat = randint(1, 6)
The data attributes are wait
and eat
.
The behavior of a customer is defined by overriding
the definition of the run
method.
from time import sleep
def run(self):
"""
Customer waits for order and eats,
writes three messages.
"""
name = self.getName()
print(name + ' is waiting for %d time units' % self.wait)
sleep(self.wait)
print(name + ' waited %d time units' % self.wait)
sleep(self.eat)
print(name + ' ate for %d time units' % self.eat)
Our script fastfood.py
works.
The benefits of using threads are twofold.
First, the object oriented design separates the
three stages in the life cycle of a thread:
born, started, and active.
Second, the script models the concurrency well,
as customers are acting independently.
The behavior is defined locally, with the data attributes
stored in each object.
Our short, first design has at least two limitations. First, the role of the server(s) is not represented and the simulation ignores the taking of the orders. Second, all data is gone with the ending of each thread, which makes the gathering of statistics rather hard.
Two Players taking Turns¶
To illustrate synchronization, consider two players, taking turns.
first player thinks, makes a move
second player thinks, makes a move
first player thinks, makes a move
second player thinks, makes a move, etc...
Instead of one main program regulating turns, we want:
- two threads independently running,
- checking and changing a shared variable.
In this desired setup, the main program plays no role anymore,
the two players are in control of the game.
The script taketurns.py
runs as follows.
$ python taketurns.py
player 0 born
player 1 born
starting the game
game has started
0 checks value 3
1 checks value 3
1 thinks 7 time units
0 checks value 3
1 changes value to 2
0 checks value 2
0 thinks 2 time units
0 changes value to 1
1 checks value 1
1 thinks 8 time units
0 checks value 1
1 changes value to 0
0 checks value 0
1 checks value 0
Let us consider the multithreaded algorithm in taketurns.py
.
After making move, the players enter a busy-waiting loop.
while True:
time.sleep(5)
if value % 2 == 'player name':
break
The sleep time of 5 units is necessary
to prevent one thread from absorbing all CPU cycles.
About the shared value
:
- It determines the total number of moves the two players make.
- After each move,
value
is decreased by one.
The main function in taketurns.py
is defined below.
def main():
"""
Defines two players '0' and '1'
and starts the game.
"""
shared = []
one = Player('0', shared)
two = Player('1', shared)
print 'starting the game'
shared.append(3)
one.start()
two.start()
print('game has started')
The role of the list shared
in the function above
requires an explanation.
The threads communicate with each other via this shared
.
To understand how this is possible, consider Fig. 92.
With shared = []
, we pass only the reference
when we create p1 = Player('0',shared)
.
The value in shared
(i.e.: shared[0]
) is set later.
We pass the reference,
not the values to the data object attributes of the threads.
The class Player
and its constructor is defined below.
from threading import Thread
class Player(Thread):
"""
Player waits turn and makes move.
"""
def __init__(self, t, v):
"""
Initializes player with name t,
and stores the shared value v.
"""
Thread.__init__(self, name=t)
print('player ' + t + ' born')
self.sv = v
What an instance of the class Player
does
when it is started is defined by the method run
.
def run(self):
"""
Player checks value every 5 time units.
If parity matches, value is decreased.
The game is over if value == 0.
"""
p = self.getName()
n = int(p)
while True:
while True:
sleep(5)
v = self.sv[0]
print(p + ' checks value %d ' % v)
if v <= 0 or v % 2 == n:
break
if v <= 0:
break # game over
nbr = randint(1, 10)
s = p + ' thinks %d' % nbr
print(s + ' time units')
sleep(nbr)
v = self.sv.pop(0) - 1
print(p + ' changes value to %d ' % v)
self.sv.append(v)
Observe that the inner loop in the run
is a busy waiting loop.
The shared variable is changed in the statements
v = self.sv.pop(0) - 1
and self.sv.append(v)
.
This is a critical section of the program.
Thread Safety
Code is thread safe if its simultaneous execution by multiple threads is correct.
One way to make a program thread safe is to ensure that only one thread changes shared data. Once this was only a concern for the operating system programmer ... Some illustrations of thread safety concerns are enumerated below:
- shared bank account,
- do not block intersection,
- access same mailbox from multiple computers,
- threads access same address space in memory.
A classical example to illustrate synchronization is the the dining philosphers problem. In this problem, the rules of the game are the following:
- Five philosophers are seated at a round table.
- Each philosopher sits in front of a plate of food.
- Between each plate is exactly one chop stick.
- A philosopher thinks, eats, thinks, eats, ...
- To start eating, every philosopher
- first picks up the left chop stick, and
- then picks up the right chop stick.
Why is there a problem? Well, unless you do not care about philosophers, there is a problem:
- every philosoper picks up the left chop stick, at the same time,
- there is no right chop stick left, every philosopher waits until a chop stick at the left becomes available, ...
and therefore, the philosophers starve.
To prevent the philosophers to starve, we have to prevent that they can acquire a resource (in this example, the right chop stick) at the same time. The request for a (shared) resource is similar to changing the value of a shared variable.
Recall the critical section in taketurns.py
in the two commands below:
v = self.sv.pop(0) - 1
self.sv.append(v)
To ensure that only one thread executes code newline in critical section, we use a lock as follows.
import _thread
lock = _thread.allocate_lock()
lock.acquire()
# code in critical section
lock.release()
In CPython, the Global Interpreter Lock, abbreviated as GIL, prevents multiple native threads from executing Python bytecodes in parallel.
Why is the GIL necessary? Well, the memory management in CPython is not thread safe. Consequently, multithreaded Python scripts experience a degrading performance on multicore processors. For parallel code in Python, use the multiprocessing module (if sufficient memory available); or depend on multithreaded libraries that run outside the GIL.
Restaurant with one Waiter¶
We end this chapter with an extension of the simulation at the beginning, extending the restaurant with a waiter. To simulate a restaurant with one waiter, consider the various stages and states the customers pass through, illustrated in Table 14.
\(\rightarrow\) | 0 | enters restaurant | |
1 | waits for table | \(\leftarrow\) | |
\(\rightarrow\) | 2 | orders food | |
3 | waits for food | \(\leftarrow\) | |
\(\rightarrow\) | 4 | eats the food | |
5 | waits for bill | \(\leftarrow\) | |
\(\rightarrow\) | 6 | pays and leaves |
In this game of taking turns, the arrows have the following meaning:
- \(\rightarrow\) : waiter waits for action of customer,
- \(\leftarrow\) : customer waits for action of waiter.
Objects and threads will be used toaimplement this simulation. Object data attribute for customer is its state. The waiter knows the state of every customer. In moving from one state to the next, we assume the following convention:
- even states: customer does it,
- odd states: action of waiter needed.
We will define two classes, one for the customer and one for the waiter. As threads run autonomously, the instances of those classes run concurrently.
The simulation script can run as follows.
give number of customers : 3
simulation starts...
1 waits for table
2 waits for table
2 orders food
0 waits for table
1 orders food
2 waits for food
0 orders food
1 waits for food
0 waits for food
1 eats food
1 waits for bill
2 eats food
0 eats food
2 waits for bill
1 pays and leaves
0 waits for bill
2 pays and leaves
0 pays and leaves
The output of the code executed above is defined in the
script restaurant.py
, its main function is below.
def main():
"""
Simulation of restaurant with one waiter.
"""
shared = []
server = Waiter('w', shared)
nbr = int(input('give number of customers : '))
eaters = []
for i in range(nbr):
eaters.append(Customer(str(i), shared))
for eater in eaters:
shared.append(0)
server.start()
for eater in eaters:
eater.start()
print("simulation starts...")
The constructor of the class Consumer
is defined below.
class Customer(Thread):
"""
Customer enters, waits for table,
orders food, waits for food, eats,
waits for bill and then leaves.
"""
def __init__(self, t, S):
"""
Initializes customer with name t,
sets initial state to zero, and
stores the shared state list S.
"""
Thread.__init__(self, name=t)
self.state = 0
self.shs = S
The string representation of a customer reports the state.
def __str__(self):
"""
Returns the string representation of self.state.
"""
result = self.getName()
if self.state == 0:
result += ' enters restaurant'
elif self.state == 1:
result += ' waits for table'
elif self.state == 2:
result += ' orders food'
elif self.state == 3:
result += ' waits for food'
elif self.state == 4:
result += ' eats food'
elif self.state == 5:
result += ' waits for bill'
else:
result += ' pays and leaves'
return result
Depending on whether the state number is even or odd,
- the customer moves to the next state, or
- the customer waits for the waiter.
This is reflected in the run
method below.
def run(self):
"""
Customer passes through stages.
Moves at even numbered states
and waits at odd numbered states.
"""
while self.state < 6:
if self.state % 2 == 0:
self.move()
else:
self.wait()
print(self)
The self.move()
moves to the next state
and is defined below.
def move(self):
"""
In even numbered states,
the customer moves up after a delay.
"""
idn = self.getName()
nbr = randint(1, 10)
sleep(nbr)
self.state = self.state + 1
self.shs[int(idn)] = self.state
The busy waiting loop is defined in the method below.
def wait(self):
"""
Needs waiter for odd stages.
"""
idx = int(self.getName())
while True:
sleep(5)
if self.state < self.shs[idx]:
break
self.state = self.state + 1
The constructor of the class Waiter
is below.
class Waiter(Thread):
"""
Waiter checks the state list and
advances odd states to next level.
"""
def __init__(self, t, S):
"""
Initializes waiter with name t,
and stores the shared state list S.
"""
Thread.__init__(self, name=t)
self.shs = S
The actions of a waiter are defined in the method below.
def run(self):
"""
Advances odd states of customers,
while otherwise busy waiting.
"""
while True:
sleep(5)
done = True
for i in range(len(self.shs)):
if self.shs[i] % 2 == 1:
sleep(randint(1, 3))
self.shs[i] = self.shs[i] + 1
if self.shs[i] < 6:
done = False
if done:
break
Multithreaded programming models customers and server
as autonomous agents. What happens is event driven,
with the events triggered by individual actors.
There is no controlling main()
.
Many extensions are needed:
- customized distributions instead of
random.randint()
; - better data encapsulation of shared state list:
- separate class to control access,
- customer can only access its own state,
- for thread safety locks are needed;
- taking turns is too rigid, allow for interrupts, e.g.: customer asking for refill or more drinks.
Exercises¶
- Extend
taketurns.py
so the shared value is the data object attribute of a class, imported by both players. - Use a class to represented the shared state list
in
restaurant.py
. - Describe how
restaurant.py
should be modified to model a restaurant with multiple waiters. - Develop a multithreaded model to simulate how elevators run like in SEO: 12 floors, 4 elevators.
- Start thinking in the small: one elevator serving 3 floors and design for change.
- Write a simulation for the dining philosophers problem. Could you observe starvation? Explain.