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 :numref:`figsharedvalue`. .. _figsharedvalue: .. figure:: ./figsharedvalue.png :align: center 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. .. topic:: 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 :numref:`tabcustwaitstages`. .. _tabcustwaitstages: .. table:: Various stages a customer passes through. +---------------------+---+-------------------+--------------------+ | :math:`\rightarrow` | 0 | enters restaurant | | +---------------------+---+-------------------+--------------------+ | | 1 | waits for table | :math:`\leftarrow` | +---------------------+---+-------------------+--------------------+ | :math:`\rightarrow` | 2 | orders food | | +---------------------+---+-------------------+--------------------+ | | 3 | waits for food | :math:`\leftarrow` | +---------------------+---+-------------------+--------------------+ | :math:`\rightarrow` | 4 | eats the food | | +---------------------+---+-------------------+--------------------+ | | 5 | waits for bill | :math:`\leftarrow` | +---------------------+---+-------------------+--------------------+ | :math:`\rightarrow` | 6 | pays and leaves | | +---------------------+---+-------------------+--------------------+ In this game of taking turns, the arrows have the following meaning: * :math:`\rightarrow` : waiter waits for action of customer, * :math:`\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.