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:

  1. The name of the customer; and
  2. 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:

  1. two threads independently running,
  2. 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.

_images/figsharedvalue.png

Fig. 92 Sharing a value via a reference defined by a list.

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:

  1. shared bank account,
  2. do not block intersection,
  3. access same mailbox from multiple computers,
  4. 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:

  1. Five philosophers are seated at a round table.
  2. Each philosopher sits in front of a plate of food.
  3. Between each plate is exactly one chop stick.
  4. A philosopher thinks, eats, thinks, eats, ...
  5. To start eating, every philosopher
    1. first picks up the left chop stick, and
    2. 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.

Table 14 Various stages a customer passes through.
\(\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:
    1. separate class to control access,
    2. customer can only access its own state,
    3. for thread safety locks are needed;
  • taking turns is too rigid, allow for interrupts, e.g.: customer asking for refill or more drinks.

Exercises

  1. Extend taketurns.py so the shared value is the data object attribute of a class, imported by both players.
  2. Use a class to represented the shared state list in restaurant.py.
  3. Describe how restaurant.py should be modified to model a restaurant with multiple waiters.
  4. Develop a multithreaded model to simulate how elevators run like in SEO: 12 floors, 4 elevators.
  5. Start thinking in the small: one elevator serving 3 floors and design for change.
  6. Write a simulation for the dining philosophers problem. Could you observe starvation? Explain.