









Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
Introduction to computer science. Handout of Programming paradigms. Thread and Semaphore Examples - Prof. Cain - Stanford University
Typology: Study notes
1 / 16
This page cannot be seen from the preview
Don't miss anything!










CS107 Handout 23 Spring 2008 May 5, 2008
Handout prose by Julie Zelenski, examples written by Nick Parlante and Julie. Semaphores Since all threads run in the same address space, they all have access to the same data and variables. If two threads simultaneously attempt to update a global counter variable, it is possible for their operations to interleave in such way that the global state is not correctly modified. Although such a case may only arise only one time out of thousands, a concurrent program needs to coordinate the activities of multiple threads using something more reliable that just depending on the fact that such interference is rare. The semaphore is designed for just this purpose. A semaphore is somewhat like an integer variable, but is special in that its operations (increment and decrement) are guaranteed to be atomic—you cannot be halfway through incrementing the semaphore and be interrupted and waylaid by another thread trying to do the same thing. That means you can increment and decrement the semaphore from multiple threads without interference. By convention, when a semaphore is zero it is "locked" or "in use". Otherwise, positive values indicate that the semaphore is available. A semaphore will never have a negative value. Semaphores are also specifically designed to support an efficient waiting mechanism. If a thread can’t proceed until some change occurs, it is undesirable for that thread to be looping and repeatedly checking the state until it changes. In this case semaphore can be used to represent the right of a thread to proceed. A non-zero value means the thread should continue, zero means to hold off. When a thread attempts to decrement a unavailable semaphore (with a zero value), it efficiently waits until another thread increments the semaphore to signal the state change that will allow it to proceed. Semaphores are usually provided as an ADT by a machine-specific package. As with any ADT, you should only manipulate the variables through the interface routines—in this case SemaphoreWait and SemaphoreSignal below. There is no single standard thread synchronization facility, but they all look and act pretty similarly. SemaphoreWait(Semaphore s) If a semaphore value is positive, decrement the value otherwise suspend the thread and block on that semaphore until it becomes positive. The thread package keeps track of the threads that are blocked on a particular semaphore. Many packages guarantee FIFO/queue behavior for the unblocking of threads to avoid starvation. Alternately the threads blocked on a semaphore may be stored as a set where the thread manager is free to choose any one. In that case, a thread could theoretically starve, but it's unlikely.
Historically, P is a synonym for SemaphoreWait. You see, P is the first letter in the word prolagen which is of course a Dutch word formed from the words proberen (to try) and verlagen (to decrease). SemaphoreSignal(Semaphore s) Increment the semaphore value, potentially awakening a suspended thread that is blocked on it. If multiple threads are waiting, it is not deterministic which will be chosen. Also there is no guarantee that any suspended thread will actually begin running immediately when awakened. The awakened thread may just be marked or queued for execution and will run at some later time. V historically is a synonym for SemaphoreSignal since, of course, verhogen means "to increase" in Dutch. No GetValue(Semaphore s) function One special thing to note about semaphores is that there is no "SemaphoreValue" function in the interface. You cannot look at the value directly, you can only operate on the value through the increment and decrement operations of Signal and Wait. It isn't really useful to retrieve the value of the semaphore since as you receive the return value there is no guarantee it hasn't been changed in the meantime by another thread. Semaphore use In client code, a SemaphoreWait call is a sort of checkpoint. If the semaphore is available (i.e. has a positive value) a thread will decrement the value and breeze right through the call to SemaphoreWait. If the semaphore is not available, then the thread will efficiently block at the point of the SemaphoreWait until the semaphore is available. A call to SemaphoreWait is usually balanced by a call to SemaphoreSignal to release the semaphore for other threads. Binary semaphores A binary semaphore can only be 0 or 1. Binary semaphores are most often used to implement a lock that allows only a single thread into a critical section. The semaphore is initially given the value 1 and when a thread approaches the critical region, it waits on the semaphore to decrement the value and "take out" the lock, then signals the semaphore at the end of the critical region to release the lock. Any thread arriving at the critical region while the lock is in use will block when trying to decrement the semaphore, because it is already at 0. When the thread inside the critical region exits, it signals the semaphore and brings its value back up to 1. This allows the waiting thread to now take out the lock and enter the critical section. The result is that at most one thread can enter into the critical section and only after it leaves can another enter. This sort of locking strategy is often used to serialize code that accesses a shared global variable.
downsides of globals—keeping track of who changes the data where is difficult, it leads to routines that have lots of interdependencies other than what is indicated by the parameter lists, and so on. As an alternative, you can declare variables as local variables within a function (most usually the main function) and then pass pointers to those variables as arguments to the new threads. This avoids the global variables and all their attendant risks and gives you direct control and documentation about which routines have access to these pieces of data. The downside is longer argument lists for the functions and more complicated variable management. You also need to be very careful here—if you are going to pass a pointer to a stack variable from one thread's stack to an another, you need to be absolutely positive that the original stack frame will remain valid for the entire time the other thread is using the pointers it was given. This can be tricky! Since the main function exists for the lifetime of the entire program, its local variables aren't at risk, but be very wary when trying to do this with any other function's local variables. We don't prefer one approach to the exclusion of the other and so our examples will show a mix of styles and you are free to adopt the one that works best for you. Binary Semaphore Example The canonical use of a semaphore is a lock associated with some resource so that only one thread at a time has access to the resource. In the example below, we have one piece of global data, the number of tickets remaining to sell, that we want to coordinate the access by multiple threads. In this case, a binary semaphore serves as a lock to guarantee that at most one thread is examining or changing the value of the variable at any given time. When the program is run, it creates a certain number of threads that attempt to sell all the available tickets. This code is written with the thread package we will be using on the Sun workstations. /*
#define NUM_TICKETS 35 #define NUM_SELLERS 4 /**
Output epic18:/usr/class/cs107/other/thread_examples>ticketSeller Seller #1 sold one (34 left) Seller #0 sold one (33 left) Seller #1 sold one (32 left) Seller #1 sold one (31 left) Seller #1 sold one (30 left) Seller #1 sold one (29 left) Seller #1 sold one (28 left) Seller #2 sold one (27 left) Seller #3 sold one (26 left) Seller #2 sold one (25 left) Seller #3 sold one (24 left) Seller #2 sold one (23 left) Seller #0 sold one (22 left) Seller #1 sold one (21 left) Seller #2 sold one (20 left) Seller #0 sold one (19 left) Seller #1 sold one (18 left) Seller #1 sold one (17 left) Seller #3 sold one (16 left) Seller #2 sold one (15 left) Seller #0 sold one (14 left) Seller #0 sold one (13 left) Seller #1 sold one (12 left) Seller #3 sold one (11 left) Seller #2 sold one (10 left) Seller #0 sold one (9 left) Seller #0 sold one (8 left) Seller #1 sold one (7 left) Seller #3 sold one (6 left) Seller #2 sold one (5 left) Seller #0 sold one (4 left) Seller #1 sold one (3 left) Seller #1 sold one (2 left) Seller #1 sold one (1 left) Seller #1 sold one (0 left) Seller #3 noticed all tickets sold! (I sold 5 myself) Seller #2 noticed all tickets sold! (I sold 7 myself) Seller #1 noticed all tickets sold! (I sold 15 myself) Seller #0 noticed all tickets sold! (I sold 8 myself) All done! Note that each time you run it, different output will result because the threads will not be scheduled in exactly the same way. Maybe next time Seller 0 will sell most of the tickets or Seller 3 will finish last. But it should always be true that exactly 35 tickets are sold, no more, no less, and that's what our use of the semaphore lock is designed to ensure.
Reader-Writer example In this classic Reader-Writer problem, there are two threads exchanging information through a fixed size buffer. The Writer thread fills the buffer with data whenever there's room for more. The Reader thread reads data from the buffer and prints it. Both threads have a situation where they should block. The writer blocks when the buffer is full and the reader blocks when the buffer is empty. The problem is to get them to cooperate nicely and block efficiently when necessary. For this problem, we will use "generalized semaphores" where the value can be any non- negative number. Zero still means "locked" and any other value means "available". The code cannot look at the value of a generalized semaphore explicitly, you can only call SemaphoreWait and SemaphoreSignal which in turn depend on the value. There is a shared, fixed-size buffer. The reader reads starting at readPt and the writer writes at writePt. No locks are required to protect these integers because only one thread concerns itself with either. The semaphores ensure that the writer only writes at writePt when there is space available and similarly for the reader and readPt. This program is written using no global variables, but instead declaring the variables in main and passing their address to the new threads.
/**
/**
for (i = 0; i < NUM_WATER; i++) ThreadNew(“Oxygen”, Oxygen, 2, oxygenReady, hydrogenReady); for (i = 0; i < 2 * NUM_WATER; i++) ThreadNew(“Hydrogen”, Hydrogen, 2, oxygenReady, hydrogenReady); RunAllThreads(); printf("All done!\n"); SemaphoreFree(oxygenReady); SemaphoreFree(hydrogenReady); } static void Hydrogen(Semaphore oxygenReady, Semaphore hydrogenReady) { SemaphoreWait(oxygenReady); SemaphoreSignal(hydrogenReady); } static void Oxygen(Semaphore oxygenReady, Semaphore hydrogenReady) { SemaphoreWait(hydrogenReady); SemaphoreWait(hydrogenReady); SemaphoreSignal(oxygenReady); SemaphoreSignal(oxygenReady); printf("Water made!\n"); } Deadlock In the above program, things will quickly grind to a halt as all threads start up and immediately start waiting on one of the two counters, which all start as zero. Every thread is then waiting for an action to be taken by another thread— we call this situation "deadlock." In the water program, we can eliminate the problem by changing the order for one of the elements. Somebody has to make the first move. If hydrogen first signals hydrogen available and then waits on the oxygen counter, it will break the deadlock because raising the hydrogen count allows the oxygen thread to get through acquiring hydrogen to signal the oxygen needed by the hydrogen thread, which will allow both the elements to move on. In a simple case like this where the deadlock is immediately obvious and infinitely reproducible, it is easy to detect and fix. However there are many more subtle cases where the deadlock may happen only rarely and can be difficult to track down and remove. The water program is a good one to practice a little concurrent debugging on. Try running it under gdb and when it gets stuck, interrupt the program and use the debugging routines to print all threads and semaphores to get a picture of what is happening during execution.
Dining Philosophers Another classic concurrency problem concerns a group of philosophers seated about a round table eating spaghetti. There are the same total number of forks as there are philosophers and one fork is placed between every two philosophers—that puts a single fork to the left and right of each philosopher. The philosophers run the traditional think-eat loop. After he sits quietly thinking for a while, a philosopher gets hungry. To eat, a philosopher grabs the fork to the left, grabs the fork to the right, eats for a time using both forks, and then replaces the forks and goes back to thinking. (There's a related problem, the Dining Programmers, where you have group of programmers sitting around a round table eating sushi with one chopstick between every two programmers.) There is a possible deadlock condition if the philosophers are allowed to grab forks freely. The deadlock occurs if no one is eating, and then all the philosophers grab the fork to their left, and then look over and wait for the right fork. Solution One simple and safe solution is to restrict the number of philosophers allowed to even try to eat at once. If you only allow (n-1) of the philosophers to try to eat, then you can show that the deadlock situation cannot occur. This correctness comes at the cost of allowing slightly less spaghetti throughput than without the restriction. The restrict- competition-to-avoid-the-deadlock can be a staple technique for avoiding deadlock. What is another way to avoid the deadlock? Hint: don't have all the philosophers follow the exact same program. /**
/**