Documente Academic
Documente Profesional
Documente Cultură
Topic: - Threads, Critical section problem, Semaphores, Bounded buffer problem, Readers –
Writers problem, Dinning Philosopher problem
Process Creation
There are two main models of process creation - the fork/exec and the spawn models. On systems that
support fork, a new process is created as a copy of the original one and then explicitly executes (exec) a
new program to run. In the spawn model the new program and arguments are named in the system call, a
new process is created and that program run directly.
Fork is the more flexible model. It allows a program to arbitrarily change the environment of the child
process before starting the new program. Typical fork pseudo-code looks like:
if ( fork() == 0 ) {
/* Child process */
else {
/* Parent process */
• Any parameters of the child process’s operating environment that must be changed must be
included in the parameters to spawn, and spawn will have a standard way of handling them. There
are various ways to handle the proliferation of parameters that results, for example AmigaDOS®
uses tag lists - linked lists of self-describing parameters - to solve the problem.
• The steps to process creation are similar for both models. The OS gains control after the fork or
spawn system call, and creates and fills a new PCB. Then a new address space (memory) is
allocated for the process. Fork creates a copy of the parent address space, and spawn creates a new
address space derived from the program. Then the PCB is put on the run list and the system call
returns.
• An important difference between the two systems is that the fork call must create a copy of the
parent address space. This can be wasteful if that address space will be deleted and rewritten in a
few instruction’s time. One solution to this problem has been a second system call, vfork, that lets
the child process use the parent’s memory until an exec is made. We’ll discuss other systems to
mitigate the cost of fork when we talk about memory management. Which is “better” is an open
issue. The tradeoffs are flexibility vs. overhead, as usual.
Threads
• Threads are lightweight processes. They improve performance by weakening the process
abstraction
• A process is one thread of control executing one program in one address space. A thread may have
multiple threads of control running different parts of a program in one address space. Because
threads expose multitasking to the user (cheaply) they are more powerful, but more complicated.
Thread programmers have to explicitly address multithreading and synchronization (which is the
topic of our next unit).
User Threads
• User threads are implemented entriely in user space. The programmer of the thread library writes
code to synchronize threads and to context switch them, and they all run in one process. The
operating system is unaware that a thread system is even running.
• User-level threads replicate some amount of kernel level functionality in user space. Examples of
user-level threads systems are Nachos and Java (on OSes that don’t support kernel threads).
Becuase the OS treats the running process like any other there is no additional kernel overhead for
user-level threads. However, the user-level threads only run when the OS has scheduled their
underlying process (making a blocking system call blocks all the threads.)
Kernel Threads
Some OS kernels support the notion of threads and schedule them directly. There are system calls to
create threads and manipulate them in ways similar to processes. Synchronization and scheduling may be
provided by the kernel. Kernel-level threads have more overhead in the kernel (a kernel thread control
block) and more overhead in their use (manipulating them requires a system call). However the
abstraction is cleaner (threads can make system calls independently).
Race Conditions
An atomic action is an indivisible action - an action taken by a process than cannot be interrputed by a
context switch. When accesses to a shared variable are not atomic, call it a race condition. Consider the
following simple example:
process_1 () {
x = x +1;
print x;
process_2 () {
x = x +1;
print x;
Note that although x = x +1 looks like one statement, it probably compiles into at least 2 and
LOAD r1 x
ADD r1+1 r1
STORE r1 x
Because of this, depending on when context switches occur, any of the following sets of output and final
values in x are possible:
1 This is called multiprogramming or multitasking. Multiprocessing refers to the contition in which many
CPUs, each running a separate program have access to the same memory and peripherals. Multiprocessing
synchronization is a related, but slightly harder problem.
455
545
444
If the program is keeping bank balances, someone’s deposit just got lost. If it’s controlling chemotherapy,
maybe their life.
Only code that is manipulating shared data needs to worry about them. The most common way to avoid
race conditions is to mark the sections of code that are accessing shared data (such parts are called critical
sections or critical regions) and insure that only one process is ever executing code in the critical section
or context switched out of code in the critical section.
Critical sections are specific to shared data. If a and b are shared variables that are unrelated to each
other, one process can be in a critical section that accesses variable a while another process is in a critical
section that accesses b. Related variables must share mutual exclusion. If a program invariant is that two
varibles add to a constant, they must share mutual exclusion. For simplicity, We will talk about ways to
enforce mutual exclusion as though there is only one set of related shared variables. We’ll define 2
functions, enter_region and leave_region that guarantee that at most one process has returned from
enter_region and not leave_region, with the assumption being that a process will call enter_region before
entering the critical region and leave_region immediately after leaving it.
Mutual Exclusion (Simple & Wrong)
int flag;
while ( flag == 1 );
flag = 1;
flag = 0;
That code’s wrong, and understanding why it’s wrong will start you on your way to understanding mutual
exclusion. The code fails because flag is as much a shared variable as anything in the critical section. The
same way that two processes increment x in and leave it with incremented once from the point of view of
an external observer in our first example, two processes can both see the flag as zero before it’s set to one,
and both enter the critical section.
Structure of process Pi ----Each process has a code segment, called the critical section, in which the shared
data is accessed.
repeat
untilfalse
Problem
Ensure that when one process is executing in its critical section, no other process is allowed to execute in
its critical section.
Solution: Critical Section Problem -Requirements
Mutual Exclusion
If process Pi is executing in its critical section, then no other processes can be executing in their critical
sections.
Progress
If no process is executing in its critical section and there exists some processes that wish to enter their
critical section, then the selection of the processes that will enter the critical section next cannot be
postponed indefinitely.
Bounded Waiting
A bound must exist on the number of times that other processes are allowed to enter their critical sections
after a process has made a request to enter its critical section and before that request is granted.
repeat
entry section
critical section
exit section
remainder section
untilfalse
Algorithm 1
*Shared Variables:
*varturn: (0..1);
initially turn= 0;
*Process Pi
repeat
critical section
turn :=j;
remainder section
untilfalse
Algorithm 2
*Shared Variables
*Process Pi
repeat
flag[i] :=true;
critical section
flag[i]:=false;
remainder section
untilfalse
*Shared Variables
*Process Pi
repeat
flag[i] :=true;
critical section
flag[i]:=false;
remainder section
untilfalse
Algorithm 4
*Process Pi
repeat
flag[i] :=true;
turn:= j;
critical section
flag[i]:=false;
remainder section
untilfalse
Bakery Algorithm
Notation -
• Mutual exclusion solutions presented depend on memory hardware having read/write cycle.
• If multiple reads/writes could occur to the same memory location at the same time, this would not
work.
• Processors with caches but no cache coherency cannot use the solutions
• In general, it is impossible to build mutual exclusion without a primitive that provides some form
of mutual exclusion.
• How can this be done in the hardware???
• Test and modify the content of a word atomically -Test-and-set instruction
functionTest-and-Set(vartarget: boolean): boolean;
begin
Test-and-Set:= target;
target:= true;
end;
Process Pi
repeat
critical section
lock :=false;
remainder section
untilfalse;
varj : 0..n-1;
key : boolean;
repeat
waiting [i ] := false;
critical section
j := j + 1 mod n;
elsewaiting[j] :=
false;
remainder section
untilfalse
SEMAPHORES
A semaphore is a higher level mechanism for controlling concurrent access to a shared resources.
Instead of using operations to read and write a shared variable, a semaphore encapsulates the necessary
shared data, and allows access only by a restricted set of operations. Because a semaphore has the power
to suspend and wake processes, semaphores and similar higher-level constructs require the co-operation of
the operating system and/or programming language run-time system. There are two operations on a
semaphore S. Worker processes can wait() or signal() a semaphore. For historical reasons, the wait and
signal operations are sometimes abbreviated as P and V respectively.
Note that with semaphores, worker processes do not execute a potentially wasteful busy-waiting loop.
SEMAPHORE OPERATIONS
Wait(): a process performs a wait operation to tell the semaphore that it wants exclusive access to the
shared resource. If the semaphore is empty, then the semaphore enters the full state and allows the
process to continue its execution immediately. If the semaphore is full, then the semaphore suspends the
process (and remembers that the process is suspended).
Signal(): a process performs a signal operation to inform the semaphore that it is finished using the shared
resource. If there are processes suspended on the semaphore, the semaphore wakes one of the up. If there
are no processes suspended on the semaphore, the semaphore goes into the empty state.
TYPES OF SEMAPHORES
The semaphore we have described is a binary semaphore. It allows only one process at a time to access the
shared resource. The textbook desribes a more general semaphore that allows N > 1 processes to access
the resource simultaneously. Instead of having states empty and full, it uses a counter S. The counter is
initialised to N, with S = 0 corresponding to the full state.
Note that different implementations of semaphores use different policies for choosing which process to
wake during a signal action. A blocked set semaphore maintains a set of waiting processes and wakes one
at random; while a blocked queue semaphore wakes processes in the order in which then were suspended.
MONITORS
In Java for instance, every object is potentially a monitor. Monitor operations are specified by putting
them in a method and marking the method with the synchronized keyword.
Consider:
process producer {
while (true) {
empty.acquire();
mutex.acquire();
// produce
mutex.release();
full.release();
process consumer {
while (true) {
full.acquire();
mutex.acquire();
// consume
mutex.release();
empty.release();
The semaphore mutex provides mutual exclusion for access to the buffer.
Semaphore mutex = 1;
Semaphore db = 1;
int readerCount = 0;
process writer {
db.acquire();
// write
db.release();
process reader {
// protecting readerCount
mutex.acquire();
++readerCount;
if (readerCount == 1)
db.acquire();
mutex.release();
// read
// protecting readerCount
mutex.acquire();
--readerCount;
if (readerCount == 0)
db.release;
mutex.release();
readerCount is a <cs> over which we must maintain control and we use mutex to do so.
n philosophers sit around a table thinking and eating. When a philosopher thinks she does not interact with
her colleagues. Periodically, a philosopher gets hungry and tries to pick up the chopstick on his left and on
his right. A philosopher may only pick up one chopstick at a time and, obviously, cannot pick up a
chopstick already in the hand of neighbor philosopher.
The dining philosophers problems is an example of a large class or concurrency control problems; it is a
simple representation of the need to allocate several resources among several processes in a deadlock-free
and starvation-free manner.
A semaphore solution:
process philosopher_i {
while (true) {
chopstick[(i+1) % 5].acquire();
// eat
chopstick[i].release();
chopstick[(i+1) % 5].release();
// think
Note :-This solution guarantees no two neighboring philosophers eat simultaneously, but has the
possibility of creating a deadlock