CS-350: Fundamentals of Computing Systems Page 1 of 16 Lecture Notes

Concurrency and : Semaphores in Action

In our coverage of functionality and implementation, we have seen a number of ways that semaphores could come in handy in managing concurrency amongst processes sharing resources or data structures. Broadly speaking, we have seen that semaphores could be used in the following ways: • A binary semaphore initialized to 1 could be used to ensure , which allows a set of processes to correctly access shared variables such as counters. • A binary semaphore initialized to 0 could be used to force an instruction in one to wait for the execution of an instruction in another process. This is an example of how semaphores can be used for signaling between processes. • A counting semaphore initialized to K could be used to enforce an upper limit on the number of concurrent uses of a given resource (or service or threads of execution). We will now consider a number of classical problems that are frequently encountered when building computing systems and we will see how semaphores could be used to manage the synchronization issues that arise.

The Producer-Consumer Problem In many systems, it is often the case that one process (or more) will be consuming results from one other process (or more). The question is, what sort of synchronization do we need between a (set of) producer(s) and a (set of) consumer(s)? In the following, we will focus on one producer and one consumer, noting that our discussion applies to the general case in which multiple producers and/or consumers exist. First off, let us discuss how the producer and consumer might be communicating. We note that there must be a shared data structure—a buffer—in which the producer “puts” its output and from which the consumer “takes” its input. To that end, we ask the following questions: 1. Are there shared data structures that must be accessed in a mutually exclusive fashion? 2. Is it ever the case that a consumer will wait for a producer? If so, when? 3. Is it ever the case that a producer will wait for a consumer? If so, when? Regarding the first question, we note that the producer and consumer share access to the buffer data structure. Since we do not want this buffer to be left in an inconsistent state, we will require that any access to the buffer be in a mutually exclusive fashion. For that purpose, we will use a mutual exclusion semaphore—let’s call it mutex—which will be initialized to 1. Regarding the second question, we do not want a consumer to actively seek to consume results if there are none to consume. In other words, if there is nothing to consume, a consumer must

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 2 of 16 Lecture Notes

sleep (or block). Thus, a consumer must wait for a producer “if there is nothing to consume in the buffer”. Regarding the third question, as long as a producer is able to deposit its output in the buffer (for consumers to consume) it does not have to wait. In other words, if the buffer is assumed to be infinite in size, then it will never be the case that a producer will have to wait. If the buffer is finite in size, then a producer must wait if the buffer is “full,” so as not to cause a buffer overflow (or overrun).

Infinite Buffer Case: Consider the infinite buffer case. How do we ensure that a consumer will wait for a produced result? This is an instance of one process signaling another. If we use a semaphore out (initialized to 0) for that purpose, then upon producing an output, a producer will signal the out semaphore, and prior to consuming an output, a consumer must wait for the out semaphore. This suggests the code snippets shown in Figure 1.

Initialization: sempahore mutex:=1; semaphore out:=0;

Producer: Consumer: repeat repeat [produce v]; wait(out); wait(mutex); wait(mutex); put(v); w:=take(); signal(mutex); signal(mutex); signal(out); [consume(w)]; forever forever

Figure 1 Producer-Consumer synchronization when buffer is infinite.

Finite Buffer Case: With a finite buffer, we must introduce a mechanism that forces the producer to block if there is no space for it to put an item in the buffer. To that end, we use a counting semaphore space which we will use to keep track of available buffer space. Initially, this semaphore will be initialized to k, which is the initial size of the buffer. Before the producer adds an item to the buffer, it should wait on the space semaphore and after the consumer takes an item from the buffer, it should signal the space semaphore (signifying the addition of new space by virtue of the consumption of an item). This suggests the code snippets shown in Figure 2.

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 3 of 16 Lecture Notes

Initialization: semaphore mutex:=1; semaphore out:=0; semaphore space:=k;

Producer: Consumer: repeat repeat [produce v]; wait(out); wait(space); wait(mutex); wait(mutex); w:=take(); put(v); signal(mutex); signal(mutex); signal(space); signal(out); [consume(w)]; forever forever

Figure 2 Producer-Consumer synchronization when buffer is finite of size k.

[Exercise: Is it OK if we swap the two wait() instructions in the producer and/or consumer code? Is it OK to swap the signal() instructions in the producer and/or consumer code? Explain.]

One detail which is not spelled out in the code snippets in Figure 1 and Figure 2 is the specific implementation of the buffer data structure, and thus the specification of the put() and take() functions. We note that this is straightforward. For instance, a finite buffer could be implemented with a circular buffer b[] with two pointers in and out, whereby in is a pointer to the location in b[] into which a new item may be inserted, and out is a pointer to the location in b[] from which a produced item may be consumed as illustrated in Figure 3 and as specified by the pseudo code implementation of put() and take() shown in Figure 4.

Figure 3 Implementation of a circular buffer of size k with two pointers in and out.

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 4 of 16 Lecture Notes

put(v): b[in]:=v; in:=(in+1)mod k;

take(): w:=b[out]; out:=(out+1)mod k; return w;

Figure 4 Implementation of the put() and take() functions on the circular buffer in Figure 3.

It is important to note that our solution of the producer-consumer problem extends with little change to multiple producers and/or multiple consumers.

The Readers-Writers Problem In the producer consumer problem, we dealt with a “consumable” data which is produced once and consumed once. We now turn our attention to questions of “access” to data. The simplest forms of access to data are the read and write access. A data item could be any set of bytes— for example a file in a file system, a record or a table in a database, or a page in main memory. Without loss of generality, we assume that we are interested in read/write operations on a file, where it should be understood that by “file” we mean the unit of access granularity. Before proceeding further, let us review what is meant by a read operation on a file, and what is meant by a write operation on a file. Basically, the difference between reading a file and writing a file is that reading a file does not change the content of the file, whereas writing a file does. The above distinction between reading a file and writing a file suggest that if we are keen on ensuring the consistency of the file, we should never allow two concurrent write operations on a file. In other words, we cannot allow two “writers” of the file to gain access to the file concurrently. On the other hand, there is no harm in letting two or more “readers” access the file concurrently. We now ask ourselves the following questions regarding how readers and writers should synchronize their access to a file. 1. When should a reader block? 2. When should a writer block? To answer the first question, we note that a reader should only block if a writer is currently accessing the file. To answer the second question, we note that a writer should block if either one or more readers are currently accessing the file, or if a writer is currently accessing the file. One possible solution for the readers/writers problem is to simply enforce a mutual exclusion policy on file access. Specifically, we use a single binary semaphore (call it “”) initialized

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 5 of 16 Lecture Notes

to 1 and require that any reader and any writer wait on that semaphore before reading/writing the file. In addition, they must signal that semaphore upon completion of reading/writing. This solution is illustrated in Figure 5.

Initialization: semaphore lock :=1;

Reader: Writer: repeat repeat wait(lock); wait(lock); READ THE FILE WRITE THE FILE signal(lock); signal(lock); forever forever

Figure 5 A mutual-exclusion solution to the readers-writers problem.

In terms of preserving the consistency of the file being read/written, the mutual-exclusion solution shown in Figure 5 is correct. But, is it a good solution? The answer is no. We note that the above solution does not allow for two readers to concurrently access the file. This is too restrictive. Just think about access to the indexing structure of a database application. Why shouldn’t multiple transactions who simply want to use such a structure to navigate the database proceed concurrently? Similarly, think about access to (say) the lecture notes for this class. Why shouldn’t multiple students attempting to read the same lecture proceed concurrently? Indeed, an important consideration in synchronization problems is that the solutions we devise allow for the maximal amount of concurrency without sacrificing correctness. With respect to the (correct but overly restrictive) solution of the readers/writers problem shown in Figure 5, how could we allow multiple readers to proceed? We note that all that a reader needs to establish is that the lock is available. If the reader is the only reader in the system, then clearly, it has to possess the “lock” semaphore before it is allowed to read the file. However, if that reader can establish that some other reader has acquired the lock, then it can simply go ahead and read the file. This suggests that we should keep a count of the number of readers in the system. If that number is positive, then a reader can simply proceed to read the file. If that number is zero, then a reader should first acquire the “lock” semaphore as before. Of course, when a reader is done reading the file, it should decrement the count of readers. Lastly,, the final reader should release the lock. This is shown in the code snippets in Figure 6. Notice that in the solution shown in Figure 6, readers must coordinate their access to the readers count (the integer readers) using a mutual exclusion semaphore mutex (initialized to 1). This ensures the integrity of access to that variable.

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 6 of 16 Lecture Notes

Reader: Writer: repeat repeat wait(mutex); wait(lock); readers++; WRITE THE FILE if(readers==1) signal(lock); wait(lock); forever signal(mutex); READ THE FILE wait(mutex); readers--; if(readers==0) signal(lock); signal(mutex); forever

Figure 6 Solution of the readers/writers problem in which readers have priority

The solution shown in Figure 6 is correct, but it suffers from the following problem: In an environment with many readers, it is possible for writers to be shut out from ever gaining access to the file! Indeed, the solution in Figure 6 can be seen as allowing readers to “gang up” and keep the lock on the file amongst them. In that sense, it is a solution that favors readers. How can we fix that problem? How could we guarantee that writers will eventually be allowed to access the file? One possible solution is to reverse the asymmetry by giving priority to writers. One such solution is shown in Figure 7. • As before, the solution in Figure 7 requires that any writer or set of readers acquire the “lock” semaphore before being able to access the file. The lock semaphore is initialized to 1. • As before, a reader will proceed if it can verify that there are other readers in the system (by checking on a readers count variable, which is accessed in a mutually exclusive fashion, thanks to the mutex1 binary semaphore (initialized to 1). • In order to allow a writer to block any newly arriving readers (to avoid the starvation potential we have observed in the solution in Figure 6) we introduce a new binary semaphore “turn.” Before a reader is allowed to add itself to the set of readers that are potentially in the system, it must be able to get the turn semaphore. This turn semaphore is grabbed by a writer upon arrival and is not released until all writers are

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 7 of 16 Lecture Notes

done with file access. This is done by keeping a count of the number of writers in the system (waiting for mutually exclusive access to the file). • The count of the number of writers in the system is accessed in a mutually exclusive fashion, thanks to the mutex2 semaphore (initialized to 1).

In other words, the turn semaphore in Figure 7 allows writers to keep readers from “ganging up on them.” In effect, however, it allows writers to “gang up” on readers because once writers grab the “turn” semaphore, readers will be locked out until there are no writers in the system.

Reader: Writer: repeat repeat wait(turn); wait(mutex2); wait(mutex1); writers++; readers++; if(writers==1) if(readers==1) wait(turn); wait(lock); signal(mutex2); signal(mutex1); wait(lock); signal(turn); WRITE THE FILE READ THE FILE signal(lock); wait(mutex1); wait(mutex2); readers--; writers--; if(readers==0) if (writers==0) signal(lock); signal(turn); signal(mutex1); signal(mutex2); forever forever

Figure 7 Solution of the readers/writers problem in which readers cannot gang up in an arbitrary fashion

[Exercise: Is it correct to use one mutex semaphore instead of mutex1 and mutex2? Why and why not?]

The solution in Figure 7 allows writers to interrupt the flow of readers through the use of the turn semaphore. However, in a system with a very large proportion of readers, it is possible for readers to grab the turn semaphore back too soon—before all writers in the system have had a chance to access the file. One way to remedy this is to restrict the competition for the turn

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 8 of 16 Lecture Notes

variable so that at most 1 reader is allowed to compete for it. This gives writers a higher chance of grabbing that semaphore. • This solution is shown in Figure 8, in which a counting semaphore z (initialized to 1) ensures that no more than one reader will compete for the turn semaphore at the same time. In other words, the solution in Figure 8 ensures that, upon arrival to the system, a writer will never have to wait for more than one “extra” reader to go through the system.

Reader: Writer: repeat repeat wait(z); wait(mutex2); wait(turn); writers++; wait(mutex1); if(writers==1) readers++; wait(turn); if(readers==1) signal(mutex2); wait(lock); wait(lock); signal(mutex1); WRITE THE FILE signal(turn); signal(lock); signal(z); wait(mutex2); READ THE FILE writers--; wait(mutex1); if (writers==0) readers--; signal(turn); if(readers==0) signal(mutex2); signal(lock); forever signal(mutex1); forever

Figure 8 Solution of the readers/writers problem in which writers have priority

[Exercise: What can you say about the code in Figure 8 if we initialize the z semaphore to a positive integer C?]

The Barbershop Problem A classical problem used to illustrate the various ways in which semaphores are used for synchronization is the “Sleeping Barber Problem” and variations thereof—often called “Barbershop problems.” The Sleeping Barber Problem, which was first proposed1 by Dijkstra

1 Edsger W. Dijkstra, "Co-operating Sequential Processes", in F. Genuys (ed.), Programming Languages, Academic Press, 1968, pp. 43-112.)

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 9 of 16 Lecture Notes

in 1968, considers a barbershop in which a number of barbers work and which has a number of barber chairs as well as a number of seats (or sofa space) for customers to wait for service. The following is yet another variant of this classic illustrative problem of semaphores in action. The barbershop setup is illustrated in Figure 9.

Barber chairs

Cashier Entrance

Standing room Exit area Sofa

Figure 9 Barbershop set up, assuming a store with 3 barber chairs, 4 spaces on a sofa seat, and a single cash register.

Each barber follows the same work plan: • The barber “sleeps” when no customer is waiting (and when the barber is not in the middle of cutting some customer’s hair!) • The barber could be awakened by a new customer. Which barber to wake up is not specified, but potentially, there could be a protocol. • Once awake, the barber cuts the hair of a customer in the barber's chair. • When the haircut is done, the customer pays the barber at the cash register and is then free to leave. • After receiving payment, the barber calls the next waiting customer (if any). If such a customer exists, that customer sits in the barber's chair and the barber starts the next haircut. If no customer is waiting, the barber goes back to sleep.

Similarly, each customer follows the following plan of action:

• When the customer first enters the barbershop, the customer leaves immediately if more than 20 people are waiting (10 standing and 10 sitting). On the other hand, if the barbershop is not too full, the customer enters and waits. • Upon entering the shop, if at least one barber is sleeping, the customer wakes up a barber, and sits in that barber's chair.

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 10 of 16 Lecture Notes

• If all barbers are busy, the customer sits in a waiting-room chair, if one is available. Otherwise, the customer remains standing until a waiting-room chair becomes available.

There are other details and constraints that must be adhered to in coordinating the operation of a barbershop:

• The barbershop has a limited capacity set by the fire code. If the total number of customers in the barbershop reaches that limit, then any new customers must wait “outside” or else go home… • In order to avoid fights amongst barbers, there is a clear policy as to which barber should be awakened next (in case there are multiple barbers asleep and a customer walks in). While the policy is not spelled out (e.g., longest sleeping barber, or random, etc.), all customers are required to use the same policy (whatever it may be) in waking up barbers. • In order to avoid fights amongst customers, there is a clear policy as to which customer should be served next (in case there are multiple customers waiting when a barber chair becomes available). While the policy is not spelled out (e.g., use coin flips to pick a customer at random, FCFS, or customer with the least hair first, etc.), all customers are required to use the same policy (whatever it may be). • Similarly, in order to avoid fights amongst standing customers, there is a clear policy as to which customer should be seated on the waiting-room chairs (or sofa space) once one becomes available. Again, while the policy is not spelled out (e.g., random, FCFS, oldest customer first, or customer with the least hair first, etc.), all customers are required to use the same policy (whatever it may be). • To satisfy the IRS, each customer must be given a receipt for their payment and the customer should wait for that receipt before leaving the store. Whatever protocols are developed for the various players involved in the running of a barbershop, we must ensure that these protocols do not result in some undesirable outcomes. Examples of undesirable outcomes may include: • Having customers and workers involved in a deadlocked situation. • Having a customer leave the store (or be asked to leave the store) while their hair is half cut, or not cut in accordance to their wishes. • Having a customer pay for the wrong “job” (e.g., cut versus perm versus coloring, etc.).

We are now ready to tackle the problem! Simply put, we are to translate the above specifications into a separate pseudo code for the various roles involved in the operation of the barbershop (namely, barber role, customer role, and cashier role). Our solution should clearly allow for proper interaction between these various players.

Semaphores Needed for Controlling Multiprogramming Levels As specified above, the problem calls for a number of semaphores for the control of the levels of concurrency allowed with respect to specific resources in the system. Typically, one may use a counting semaphore to control the levels of concurrency allowed at various parts of the system. We enumerate these resources and the semaphores used to protect them below:

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 11 of 16 Lecture Notes

• We could use a counting semaphore (say ShopCapacity) to limit the number of customers allowed in the barbershop at any time. A customer will have to wait for that semaphore before being allowed to enter the barbershop and they must signal it on the way out. • We could use a counting semaphore (say SofaRoom) to limit the number of customers allowed to sit on the sofa at any time. Once in the barbershop, a customer will have to wait for that semaphore before being allowed to sit on the sofa and must signal it once they are invited to take a seat on the barber chair. • We could use a counting semaphore (say BarberChair) to limit the number of customers allowed to sit on barber chairs.A customer sitting on a sofa seat will have to wait for that semaphore before being allowed to sit on a barber chair and must signal it once they leave that chair upon the barber’s completion of their hair cut. • We could use a counting semaphore (say Workers) to limit the number of employees working in the barbershop at any one point in time. The various entities (threads of execution) representing the barber and cashier jobs must wait on that semaphore before being allowed to proceed. This restricts the number of active entities from ever exceeding the number of workers in the system.

Semaphores Needed for Signaling As specified above, the problem calls for a number of semaphores to allow the various entities in the store to signal progress and to wait for one another to reach specific rendezvous points. We enumerate these rendezvous points and the semaphores used to implement them below:

• A customer sitting on a barber chair will need to signal a barber the fact that it is ready for service. We will use a binary semaphore CustomerReady initialized to 0 for that purpose. • A barber done cutting a customer’s hair needs to signal that fact to the customer so that the customer can proceed to the cashier to pay. We will use a binary semaphore Finished initialized to 0 for that purpose. • A customer leaving the barber chair will need to signal to the barber the fact that the chair is now available for the barber to cleanup before the barber is able to signal the availability of that barber chair. We will use a binary semaphore Cleanup initialized to 0 for that purpose. • A customer will need to signal to a cashier that it has its money ready for payment. We will use a binary semaphore Payment initialized to 0 for that purpose. • A cashier done processing the payment from a customer needs to signal to the customer the fact that it has a receipt for the customer to pick up. We will use a binary semaphore Receipt initialized to 0 for that purpose.

Using the above set of semaphores, we provide a first attempt at the protocol to be used by the various entities: Customers, Barbers, and Cashier. This is shown in Figure 10.

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 12 of 16 Lecture Notes

Initialization Semaphore ShopCapacity:=20; Semaphore SofaRoom:=4; Semaphore BarberChair:=3; Semaphore Workers:=3; Semaphore CustomerReady:=0; Semaphore Finished:=0; Semaphore Cleanup:=0; Semaphore Payment:=0; Semaphore Receipt:=0;

Customer repeats Barber repeats wait(ShopCapacity); wait(CustomerReady); enter shop; wait(Workers); wait(SofaRoom); cut hair; sit on sofa; signal(Workers); wait(BarberChair); signal(Finished); leave sofa; wait(Cleanup); signal(SofaRoom); signal(BarberChair); sit in barber chair; signal(CustomerReady); wait(Finished); leave barber chair; Cashier repeats signal(Cleanup); wait(Payment); get cash out; wait(Workers); signal(Payment); take cash; wait(Receipt); signal(Workers); exit shop; signal(Receipt); signal(ShopCapacity);

Figure 10 “A” Solution of the barbershop problem.

The solution of the barbershop problem shown in Figure 10 is still not quite correct. In particular, we note the following serious problem: When a barber signals the fact that it is done cutting a customer’s hair, the customer who may get this signal (i.e., the one whose will

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 13 of 16 Lecture Notes

“wake up”) is not necessarily the one that the barber intended. To understand why, consider two customers: A and B. A has a lot of hair and thus it takes a barber a long time to service, whereas B is a balding customer who mostly needs a trim job and hence it takes the barber a short time to service. Now, assume that A makes it to the first barber chair and is now waiting on the “Finished” semaphore. Shortly after, B makes it to the second barber chair and is now waiting on the “Finished” semaphore as well. A few minutes later, B’s haircut is done and its barber signals the “Finished” semaphore. This would release one of the two customers. If the semaphore is implemented so as to release blocked processes in a FCFS fashion, then customer A will be released (without his hair completely cut) while customer B will have to endure more hair being cut, even though it was already finished! The problem spelled out above is due to the fact that we have all customers waiting on the same set of semaphores. To fix this problem, we will need to have customers be distinguishable in such a way that a barber (or cashier) would be able to interact with the “right” customer. One way of doing this is to distinguish the various barber chairs and to require that a customer sitting on barber chair i to wait for a different “Finished” binary semaphore (say “Finished[i]”). The signaling mix-up mentioned above is also possible when customers and cashiers signal each other for payment. It is possible for two customers to signal payment with each one of them getting the other customer’s receipt! Again, to solve this problem one needs to be careful about distinguishing between various customers.

The Dining Philosophers Problem If you develop a system in which entities are competing for resources, you must take precautions to ensure fairness. As we had eluded to when we discussed the mutual exclusion problem, a system is fair when each entity gets enough access to limited resources to make reasonable progress. Specifically, a fair system prevents starvation and . Starvation occurs when one or more entities are prevented from gaining access to a resource and, as a result, cannot make progress. Deadlock, the ultimate form of starvation, occurs when two or more entities are waiting on a condition that cannot be satisfied. Deadlock most often occurs when two (or more) entities are each waiting for the other(s) to do something. To that end, a classical problem often used to highlight the risk of starvation and deadlock is the Dining Philosophers problem. This problem is often used to illustrate various problems that may occur when many synchronized entities are competing for limited resources. As illustrated in Figure 11, the problem statement goes like this: • N philosophers (N is traditionally taken to be five) are sitting at a round table. • The philosophers spend their life either thinking or (when not thinking) eating. • In front of each philosopher is a bottom-less bowl of rice. • Between each pair of philosophers is one chopstick. • Before taking a bite of rice, an individual philosopher must have two chopsticks: one taken from the left and one taken from the right. • The philosophers must find a way to share chopsticks so that they all get to eat.

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 14 of 16 Lecture Notes

Figure 11 Illustration of the setting for the Dining Philosophers Problem

Clearly, the chopsticks are the shared resources whose use must be coordinated, since it is not proper for two philosophers to hold the same chopstick at the same time. To that end, a straightforward solution would require the use of a semaphore for each chopstick. Before grabbing a chopstick, a philosopher must wait on that chopstick’s semaphore, and once done with a chopstick, the philosopher should signal its semaphore (indicating that it is available for being picked up). Since there are two chopsticks needed, each philosopher (upon deciding it is time to eat) will wait for the chopstick on the left, and then the one on the right. When both are at hand, the philosopher will start eating, releasing both semaphores in order when done. This is shown in the code snippet shown in Figure 12.

Semaphore chopstick[1..5] := 1;

Philosopher i: repeat think; wait(chopstick[i]); wait(chopstick[i+1 mod 5]); eat; signal(chopstick[i+1 mod 5]); signal(chopstick[i]); forever

Figure 12 Deadlock-prone implementation of Dining Philosophers’ synchronization (N=5).

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 15 of 16 Lecture Notes

The solution shown in Figure 12 does not guarantee fairness in the sense that one cannot say with any confidence that a particular philosopher will not starve by virtue of being unable to get both chopsticks after some upper-bound on waiting time. Worse yet, the solution in Figure 12 is deadlock prone, which means that sooner or later all philosophers will starve, with plenty of rice in their bowls, which they cannot quite get into their stomachs! Specifically, if each philosopher grabs the stick on the right, then waits for the stick on the left, a deadlock will occur. Why did deadlock occur in this problem? How could we detect that it occurred? What could we have done to avoid/prevent having such as this one occur? All of these are questions that we must answer methodically once we introduce synchronization primitives that involve “blocking” an entity from making progress. This is our next topic related to synchronization. For now, though, we need to think about how to ensure that our dining philosophers will not starve due to a deadlock. One possible approach, of course, is to allow only one philosopher to eat at any point in time. We can do this easily, by using a mutual exclusion semaphore that all philosopher must use as shown in Figure 13.

Semaphore mutex := 1; Semaphore chopstick[1..5] := 1;

Philosopher i: repeat think; wait(mutex); wait(chopstick[i]); wait(chopstick[i+1 mod 5]); eat; signal(chopstick[i+1 mod 5]); signal(chopstick[i]); signal(mutex) forever

Figure 13 Mutual exclusion solution of Dining Philosophers’ synchronization.

The solution in Figure 13 is correct, of course, with the caveat that it does not allow for enough concurrency. Having only one philosopher eat at a time is too restrictive—clearly, with five philosophers around the table, there are two other philosophers who should not have to worry about sharing chopsticks with the currently-eating philosopher, and hence restricting them from eating limits concurrency in a way that underutilizes available resources (the chopsticks). Another concern, of course is the fact that by limiting concurrency to one (through mutual exclusion) does not scale; just think about the case when N=1,000!

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350. CS-350: Fundamentals of Computing Systems Page 16 of 16 Lecture Notes

Following on our solution using mutual exclusion, we note that if we allow any two philosophers to dine concurrently, then one can show that deadlock cannot occur, because in the worst case (when the philosophers are seated in two adjacent seats j and j+1) one of them (namely philosopher seated in seat j) is guaranteed to eat—hence no deadlock. In a similar manner, one can see that as long as we allow up to N-1 philosophers to eat at any given time, deadlock will not occur. This suggests the solution sketched in Figure 14, using a mutual exclusion semaphore that all philosopher must wait on to eat.

Semaphore eating := N-1; Semaphore chopstick[1..5] := 1;

Philosopher i: repeat think; wait(eating); wait(chopstick[i]); wait(chopstick[i+1 mod 5]); eat; signal(chopstick[i+1 mod 5]); signal(chopstick[i]); signal(eating) forever

Figure 14 Deadlock-free maximal concurrency solution of Dining Philosophers’ synchronization.

A number of other possible solutions could be suggested. For example, the problem could be solved by having philosophers have special IDs based on the number of their seat, and then requiring “even-seated” philosophers to follow a protocol that is slightly different from that followed by “odd-seated” philosophers—specifically, by having them pick chopsticks in different order (e.g., “even-seated” philosophers pick up the right chopstick followed by the left chopstick). While one can show that such approaches will work for our dining philosophers problem, it is clear that ad-hoc solutions (relying on knowledge of the specific problem at hand like this one) are not the way to go, but rather we need a disciplined approach for how to managed synchronization in such a way that does not allow for deadlocks to occur.

© Azer Bestavros. All rights reserved. Reproduction or copying (electronic or otherwise) is expressly forbidden except for students enrolled in CS-350.