Semaphores * Semaphores provide the same functionality as conditional variables: they let threads of an application coordinate with each other. A semaphore variable is initialized with a counter N. A thread/process can call up and down operations on a semaphore. A down operation on a semaphore blocks when its counter reaches 0, and returns when another thread calls up on it. Note that the up and down operations on a semaphore must be implemented atomically, either using atomic instructions or other means. * POSIX semaphores in Linux can be used much like pthreads conditional variables when building multithreaded applications, though some problems are more elegantly solved with semaphores and some with conditional variables. Note that only the down action on a semaphore blocks, and is roughly equivalent to the wait action on a conditional variable. The up action is analogous to signal and does not block. Note that a semaphore with a count of 1 (binary semaphore) is equivalent to a mutex lock. * Example of a producer and consumer problem with a buffer size of N. The producer thread must signal to the consumer thread when there is data to consume, and the consumer must signal the producer when there is buffer space to produce. Note how a mutex (or binary semaphore) should be used for mutual exclusion, along with another semaphore for signaling. sem emptyCount = N, fullCount = 0; Producer code: down(emptyCount) lock(mutex) add item to buffer unlock(mutex) up(fullCount) Consumer code: down(fullCount) lock(mutex) consume item unlock(mutex) up(emptyCount) * Consider a pipeline problem where threads A, B, C have to run in that order on a certain request. The following is the logic of thread A, using semaphores adone=0, bdone=0, cdone=1; down(cdone) do A's work //maybe with locking on the common request up(adone) * What about a pipeline of the form A-B-C-B-A? Can you solve it using semaphores and conditional variables? * The above problems solve the question of signaling between threads. Another pattern is a rendezvous or barrier between threads. Consider two threads A and B that perform two operations each. Let the operations of thread A be A1 and A2; let the operations of thread B be B1 and B2. We require that threads A and B each perform their first operation before either can proceed to the second operation. That is, we require that A1 be run before B2 and B1 before A2. We can solve it with semaphores as follows. sem A1Done = 0; sem B1Done = 0; //Thread A A1 up(A1Done) down(B1Done) A2 //Thread B B1 up(B1Done) down(A1Done) B2 * What happens if the up and down statements above where interchanged? The code would deadlock. * How would you solve this problem of a barrier when there are N threads? That is, you want each of the N threads to do the first step, and wait until all have finished the first step before they can progress to the second step. Every thread does the following: //run first step down(mutex); count++; up(mutex); if(count == N) for i = 1 to N up(step1Done); //up N times down(step1Done); //run second step ----------------------------- Inter-process synchronization and communication * Mechanisms for process to communicate with - other processes on the same machine: shared memory, signals, pipes, (unix) sockets, message queues, shared semaphores, and so on. - other processes on another machine: (TCP or UDP) sockets - disk and other I/O devices: the file abstraction. * If a multiprocess model is employed in an application, say with a master process and multiple worker processes, some form of interprocess communication and synchronization must be employed, much like in the case of multithreaded applications. * System calls to create and use shared memory segments map a shared page of memory into the page tables of two or more processes. Note that the processes must still synchronize with each other to make sure that they do not edit concurrently and corrupt the data structures in the shared memory. * Signals can convey only limited information between processes, and cannot be used to pass arbitrary messages. Every process can listen to and handle a specified number of signals. Processes come with default signal handlers, which can be overridden to do specific work when a signal arrives. The kill system call is used to send a signal to a process from the kernel or from any other process. * Message queues are equivalent to mailboxes. A process can create a message queue and read messages from it, while other processes can post messages to this mailbox. The read/write calls to the message queue can block when the queue is empty/full, or non-blocking variants can be used. * Semaphores can also be used to synchronize across processes. Two processes can use named semaphores with the same name, or store semaphores in shared memory segments to get access to common semaphores. * Unix sockets and pipes work within the I/O subsystem of the kernel. That is, sockets, pipes and files are accessed by processes using similar system calls. * The PCB has a per-process file descriptor table. When any channel of communication (file, socket, pipe) is opened, an entry is created in the file descriptor array, and the index of the array is returned as a file descriptor to the application code. When communicating over sockets, the file descriptor points to socket data structures (send and receive buffers that store data written/to be read from socket). When communicating over files, the file descriptor points to an inode (a file control block that has information on the location of file blocks and other information needed to access a file). When using pipes, the file descriptor points to a pipe buffer. * System calls to open the communication channel: open (opens a file or any other I/O device), socket (creates a socket), pipe (creates two ends of a pipe, the read end and the write end). * Once the communication channel is opened, the system calls to read and write are similar across sockets, pipes, files: read(fd, buffer) or write (fd, buffer). * How do pipes work? The pipe system call creates two file descriptors, one of which can be used to write to a pipe buffer, and the other can read from the pipe buffer. How is this useful to communicate across processes? When a parent creates multiple child processes, all the children inherit the file descriptor table. So the parent and child will both have read and write pointers to the pipe buffers. The parent and child can each close one end of the pipe and use the other end to communicate with each other. Alternately, named pipes (pipes created with the same name) can be used to setup a pipe-like communication between unrelated processes.