Concurrent Programming(3)
Using Semaphores to Schedule Shared Resources
In this scenario, a thread uses a semaphore operation to notify another thread that some condition in the program state has become true.
Producer-Consumer Problem
The producer-consumer problem is shown in Figure 12.23. A producer and con- sumer thread share a bounded buffer with n slots.
The producer thread repeatedly produces new items and inserts them in the buffer. The consumer thread repeat- edly removes items from the buffer and then consumes (uses) them.
Variants with multiple producers and consumers are also possible.
Since inserting and removing items involves updating shared variables, we must guarantee mutually exclusive access to the buffer.
But guaranteeing mutual exclusion is not sufficient. We also need to schedule accesses to the buffer.
If the buffer is full (there are no empty slots), then the producer must wait until a slot becomes available.
Similarly, if the buffer is empty (there are no available items), then the consumer must wait until an item becomes available.
Producer-consumer interactions occur frequently in real systems.
...Another common example is the design of graphical user interfaces.
The producer detects mouse and keyboard events and inserts them in the buffer. The consumer removes the events from the buffer in some priority-based manner and paints the screen.
implementation:
difference:
Readers-Writers Problem
Writers must have exclusive access to the ob- ject, but readers may share the object with an unlimited number of other readers.
Readers-writers interactions occur frequently in real systems.
...As another example, in a mul- tithreaded caching Web proxy, an unlimited number of threads can fetch existing pages from the shared page cache, but any thread that writes a new page to the cache must have exclusive access.
The readers-writers problem has several variations, each based on the priori- ties of readers and writers.
The first readers-writers problem, which favors readers, requires that no reader be kept waiting unless a writer has already been granted permission to use the object. In other words, no reader should wait simply because a writer is waiting.
The second readers-writers problem, which favors writers, re- quires that once a writer is ready to write, it performs its write as soon as possible. Unlike the first problem, a reader that arrives after a writer must wait, even if the writer is also waiting.
A correct solution to either of the readers-writers problems can result in starvation, where a thread blocks indefinitely and fails to make progress.
Other synchronization mechanisms
But you should know that other synchronization techniques exist as well.
For example, Java threads are synchronized with a mechanism called a Java monitor [51], which provides a higher level abstraction of the mutual exclusion and scheduling capabilities of semaphores; in fact monitors can be implemented with semaphores.
As another example, the Pthreads interface de- fines a set of synchronization operations on mutex and condition variables.
Pthreads mutexes are used for mutual exclusion. Condition variables are used for scheduling accesses to shared resources, such as the bounded buffer in a producer-consumer program.
Putting It Together: A Concurrent Server Based on Prethreading
In the concurrent server in Figure 12.14, we created a new thread for each new client.
A disadvantage of this approach is that we incur the nontrivial cost of creating a new thread for each new client.
A server based on prethreading tries to reduce this overhead by using the producer-consumer model shown in Figure 12.27.
The server consists of a main thread and a set of worker threads.
The main thread repeatedly accepts connection requests from clients and places the resulting connected descriptors in a bounded buffer.
Each worker thread repeatedly removes a descriptor from the buffer, services the client, and then waits for the next descriptor.
This is interesting code(The echo_cnt function ) to study because it shows you a general technique for initializing packages that are called from thread routines.
In our case, we need to initialize the byte_cnt counter and the mutex semaphore.
One approach, which we used for the Sbuf and Rio packages, is to require the main thread to explicitly call an initialization function.
Another approach, shown here, uses the pthread_once function (line 19) to call the initialization function the first time some thread calls the echo_cnt function.
The advantage of this approach is that it makes the package easier to use.
The disadvantage is that every call to echo_cnt makes a call to pthread_once, which most times does nothing useful.
All codes:
Event-driven programs based on threads
I/O multiplexing is not the only way to write an event-driven program.
For example, you might have noticed that the concurrent prethreaded server that we just developed is really an event-driven server with simple state machines for the main and worker threads.
The main thread has two states (“waiting for connection request” and “waiting for available buffer slot”), two I/O events (“connection request arrives” and “buffer slot becomes available”), and two transitions (“accept connection request” and “insert buffer item”).
Similarly, each worker thread has one state (“waiting for available buffer item”), one I/O event (“buffer item becomes available”), and one transition (“remove buffer item”).