Lab3 Synchronization
Lab3 Synchronization
Lab3 Synchronization
Lab 3
Synchronization
Course: Operating Systems
April 1, 2024
Goal This lab helps the student practice synchronization in the operating system, and figure out why
we need the synchronization.
Contents In detail, this lab requires the student practice experiments using synchronization techniques
to solve the problem called race condition. The synchronization is performed on Thread using the
following mechanisms:
• semaphore
• conditional variable
Besides, the practices also introduce and include some locking variants, i.e. spinlock, read/write spinlock,
sequence lock.
Result After doing this lab, student can understand the definition of synchronization and write a
program without the race condition using the techniques above.
1
CONTENTS CONTENTS
Contents
1 Background 3
1.1 Race condition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1.1 Critical section problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1.2 Race condition caused by atomicity . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.1.3 Race condition caused by in-correct ordering . . . . . . . . . . . . . . . . . . . . . 4
2 Programming Interfaces 6
2.1 Mutex lock . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Spin lock . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.3 Semaphore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.4 Conditional Variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.5 Reader-writer lock . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.6 Sequence lock . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3 Practices 9
3.1 Shared buffer problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.2 Bounded buffer problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
4 Exercise 12
2
1 BACKGROUND
1 Background
1.1 Race condition
Race condition is the condition of software where the systems’substain behaviors is dependent on the
sequences of uncontrollable events. Therefore, we need mechanisms that provide synchronization ability.
Consider the system includes n tasks, represented by threads or processes with shared memory, denoted
by {P0 ,P1 ,...,Pn−1 }
Each process has a critical section (i.e, segment of code) which has the following properties:
In this work, we investigate the case of multi-thread process in which the shared data segment is
natively encountered the problem of race condition. The structure of multi-thread process is illustrated
in Figure 1
register PC stack
register register register
PC PC PC
Critical-section problem needs to design a protocol to solve the raised problem of race condition. To
propose the solution, we need first able to figure out/identify critical section in each program code, then
we employs the synchronization mechanisms which are introduced/practiced in this work.
3
1.1 Race condition 1 BACKGROUND
In high level programming language, we can note that a statement is always implemented in machine
language (on a typical machine) by many instuctions as the following example:
i n s t r u c t i o n 1 := r e g i s t e r l o a d
i n s t r u c t i o n 2 := a r i t h m e t i c o p e r a t i o n
i n s t r u c t i o n 3 := r e g i s t e r s t o r e
If there are two or more tasks (threads/or shared-memory processes) accessing a single storage, the
data manipulation which is splitable may result in an incorrect final state. Such tools are provided by
POSIX pthread as follows:
Mutex Lock The problem with mutex is that the task is put into a sleep state and later is woken
to process the task. This sleeping and waking action are expensive operations.
Spin lock In a short description, spin lock provides a (CPU) poll waiting until it has got previliege.
If the mutex sleeps in a very short time, then it wastes the cost of expensive operations of sleeping
and waking up. But if the long time is quite long, CPU polling is a big waste of computation.
STORAGE
thread 1
data
thread 2
instruction 1 instruction 1
instruction 2
instruction 1 ...
} lock {
instruction 2 instruction 1
instruction 2
instruction 2 ...
}
RACE CONDITION
In the previous chapter of process memory sharing, we introduced the bounded buffer problem. Even
with mutex setting, the race conditional may still happen when a producer fills in a buffer an item before
a consumer empties it causing an overwriting which results a loosing data. Another example of system
behavior happens when a consumer retrieves a garbage (uninitialized) value if an item is retrieved before a
producer fills in a meaningful value. These wrong system behaviors are caused by the in-correct ordering
of the sequence of events.
4
1.1 Race condition 1 BACKGROUND
counting_semaphore counting_semaphore
slot = N item=0
Producer Producer
Consumer Consumer
Producer Producer
Consumer Consumer
Producer Producer
signal(item) signal(slot)
Although semaphore is perfect fit for bounded buffer problem, it constrains on the equal number of
consumers(/readers) and producers(/writer). Some updated models such as few writers many readers is
not a good application for semaphore. It relaxes the matching of internal counter with the number of
slot then it helps in the unbalancing context between actors who access the buffer. These following tools
provide the ordering mechanism with more relaxation on the number of system actors:
Conditional variable Conditional variable is a synchronization primitive that allows threads to wait
until a particular condition occur. It just allows a thread to be signaled when something of interest to
that thread occurs. It includes two operations: wait and signal. The conditional variable can be used by
a thread to block other threads until it notifies the conditional variable.
Read-write spin lock and sequence lock In the previous section, we have seen some locking methods
like mutex, spinlock, etc. In a high-speed manner, i.e., kernel driver, high-speed/fast communication,
when you want to treat both the reader and the writer equally, then you have to use spin lock. The two
following mechanisms provide a different priority policy between reader and writer. We introduce the
main characteristics of them and then, we discuss the reader/writer conflict problem later as an exercise.
(See more details in Section4)
Read-write spinlock: In some situations, we may have to give more access frequencies to the
reader. The reader-writer spinlock is a suitable solution in this case.
Sequence lock: Reader-writer lock can cause writer starvation. Seqlock gives more permission to
writer. Sequential lock is a reader-writer mechanism which is given high priority to the writer, so this
avoids the writer starvation problem.
5
2 PROGRAMMING INTERFACES
2 Programming Interfaces
POSIX Thread library provides a standard based thread API for C/C++. The functions and declara-
tions of many common APIs in this library are conformed to POSIX.1 standards through many versions
(2003, 2017 etc.). In Linux, the library is not integrated by default, so it requires an explicit linking
declaration with “-pthread”. Semaphore is belong to POSIX (not pthread) standard. Sequence lock is
not implemented in the both libraries but has been added to the kernel since Linux 2.5.x.
int p t h r e a d m u t e x i n i t ( p t h r e a d m u t e x t ∗mutex ,
const p t h r e a d m u t e x a t t r t ∗ a t t r ) ;
int p t h r e a d m u t e x d e s t r o y ( p t h r e a d m u t e x t ∗mutex ) ;
int p t h r e a d m u t e x l o c k ( p t h r e a d m u t e x t ∗mutex ) ;
int p t h r e a d m u t e x u n l o c k ( p t h r e a d m u t e x t ∗mutex ) ;
An example:
pthread mutex t lock;
pthread mutex init(&lock,NULL);
...
pthread mutex lock(&lock);
< CS >
pthread mutex unlock(&lock);
< RS >
int p t h r e a d s p i n i n i t ( p t h r e a d s p i n l o c k t ∗ l o c k , int p s h a r e d ) ;
int p t h r e a d s p i n d e s t r o y ( p t h r e a d s p i n l o c k t ∗ l o c k ) ;
int p t h r e a d s p i n l o c k ( p t h r e a d s p i n l o c k t ∗ l o c k ) ;
int p t h r e a d s p i n u n l o c k ( p t h r e a d s p i n l o c k t ∗ l o c k ) ;
An example:
pthread spinlock t lock;
6
2.3 Semaphore 2 PROGRAMMING INTERFACES
//we can use pshared=0 for NULL setting or pshared=PTHREAD PROCESS SHARED
pthread spin init(&lock,PTHREAD PROCESS SHARED);
...
pthread spin lock(&lock);
< CS >
pthread spin unlock(&lock);
< RS >
2.3 Semaphore
provided in POSIX semaphore (not PTHREAD)
#include <semaphore . h>
sem t sem ;
An example:
sem t sem;
//we can use pshared=0 for NULL setting or pshared=PTHREAD PROCESS SHARED
//The 3rd argument is the default value of the semaphore
sem init(&sem,0,5);
...
sem wait(&sem);
< CS >
sem post(&sem);
< RS >
An example:
pthread mutex t mtx;
pthread cond t lock;
7
2.5 Reader-writer lock 2 PROGRAMMING INTERFACES
int p t h r e a d r w l o c k i n i t ( p t h r e a d r w l o c k t ∗ rwlock ,
const p t h r e a d r w l o c k a t t r t ∗ a t t r ) ;
int p t h r e a d r w l o c k d e s t r o y ( p t h r e a d r w l o c k t ∗ r w l o c k ) ;
int p t h r e a d r w l o c k r d l o c k ( p t h r e a d r w l o c k t ∗ r w l o c k ) ;
int p t h r e a d r w l o c k w r l o c k ( p t h r e a d r w l o c k t ∗ r w l o c k ) ;
int p t h r e a d r w l o c k u n l o c k ( p t h r e a d r w l o c k t ∗ r w l o c k ) ;
An example:
pthread rwlock init(&lock,NULL);
...
pthread rwlock rdlock(&lock);
< CS >
pthread rwlock unlock(&lock);
< RS >
...
pthread rwlock rdlock(&lock);
< CS >
pthread rwlock wrunlock(&lock);
< RS >
Although it lacks a userspace implementation, it is widely used in the kernel to protect buffer data in
modern programming patterns with many readers, many writers, .i.e SMP Linux kernel support.
8
3 PRACTICES
3 Practices
In this section, we work on various ”native” problems to regcognize the ”real” wrong behaviors. All
these experiments are derived from theory slides with a minor modification. We practice the provided
synchronization mechanisms and see how they work to correct the wrong things.
i n t MAX COUNT = 1 e 9 ;
s t a t i c int count = 0 ;
void ∗ f c o u n t ( void ∗ s i d ) {
int i ;
f o r ( i = 0 ; i < MAX COUNT; i ++) {
count = count + 1 ;
}
i n t main ( ) {
p t h r e a d t thread1 , thread2 ;
/∗ C r e a t e i n d e p e n d e n t t h r e a d s e a c h o f w h i c h w i l l execute f u n c t i o n ∗/
p t h r e a d c r e a t e ( &t h r e a d 1 , NULL, &f c o u n t , ” 1 ” ) ;
p t h r e a d c r e a t e ( &t h r e a d 2 , NULL, &f c o u n t , ” 2 ” ) ;
// Wait f o r t h r e a d t h 1 f i n i s h
p t h r e a d j o i n ( t h r e a d 1 , NULL ) ;
// Wait f o r t h r e a d t h 1 f i n i s h
p t h r e a d j o i n ( t h r e a d 2 , NULL ) ;
return 0 ;
}
Step 3.1.2 Recognize the wrong issue and propose a fix mechanism using the provided synchronization
tool.
Hint: pthread mutex lock() and pthread mutex unlock() are useful to protect f count() thread worker
# Implement by your s e l f t h e f i x e d shrdmem mutex program
# ( i t i s not a v a i l a b l e y e t )
# c o p y c a t may r e s u l t e r r o r g c c : f a t a l e r r o r : no i n p u t f i l e s
$ g c c - p t h r e a d - o shrdmem mutex shrdmem . c
$ . / shrdmem mutex
Thread 2 : h o l d i n g 1001990720
Thread 1 : h o l d i n g 2000000000
9
3.2 Bounded buffer problem 3 PRACTICES
#d e f i n e BUF SIZE 2
#d e f i n e THREADS 1 // 1 p r o d u c e r and 1 c o n s u m e r
#d e f i n e LOOPS 3 ∗ BUF SIZE // v a r i a b l e
// I n i t i a t e s h a r e d b u f f e r
i n t b u f f e r [ BUF SIZE ] ;
int f i l l = 0 ;
int use = 0 ;
void ∗ p r o d u c e r ( void ∗ a r g ) {
int i ;
int t i d = ( int ) arg ;
f o r ( i = 0 ; i < LOOPS ; i ++) {
/∗TODO: F i l l i n t h e s y n c h r o n i z a t i o n s t u f f ∗/
put ( i ) ; // l i n e P2
p r i n t f ( ” P r o d u c e r %d put d a t a %d\n” , t i d , i ) ;
sleep (1);
/∗TODO: F i l l i n t h e s y n c h r o n i z a t i o n s t u f f ∗/
}
p t h r e a d e x i t (NULL ) ;
}
i n t main ( i n t a r g c , char ∗∗ a r g v ) {
int i , j ;
i n t t i d [THREADS ] ;
p t h r e a d t p r o d u c e r s [THREADS ] ;
p t h r e a d t c o n s u m e r s [THREADS ] ;
// C r e a t e c o n s u m e r t h r e a d
p t h r e a d c r e a t e ( & c o n s u m e r s [ i ] , NULL, consumer , ( void ∗ ) tid [ i ] ) ;
}
return 0 ;
}
void put ( i n t v a l u e ) {
buffer [ f i l l ] = value ; // l i n e f 1
f i l l = ( f i l l + 1 ) % BUF SIZE ; // l i n e f 2
}
int get ( ) {
i n t tmp = b u f f e r [ u s e ] ; // l i n e g 1
b u f f e r [ use ] = - 1 ; // c l e a n t h e item
u s e = ( u s e + 1 ) % BUF SIZE ; // l i n e g 2
return tmp ;
}
10
3.2 Bounded buffer problem 3 PRACTICES
Step 3.2.2 Recognize the wrong issue and propose a fix mechanism using the provided synchronization
tool.
Hint: sem wait() and sem signal() are useful to protect consumer() and producer() thread worker
11
4 EXERCISE
4 Exercise
PROBLEM 1 Design and implement sequence lock API.
#include ” s e q l o c k . h” /∗ TODO: implement t h i s h e a d e r f i l e ∗/
/∗
∗ TODO: Implement t h e s e f o l l o w i n g APIs
∗/
pthread seqlock t lock ;
/∗ I n i t w i t h d e f a u l t NULL a t t r i b u t e a t t r=NULL ∗/
int p t h r e a d s e q l o c k i n i t ( p t h r e a d s e q l o c k t ∗ s e q l o c k ) ;
int p t h r e a d r w l o c k d e s t r o y ( p t h r e a d s e q l o c k t ∗ s e q l o c k ) ;
• When there is no thread in the critical section, any reader or writer can enter into a critical section.
But only one thread can enter.
• If the reader is in critical section, the new reader thread can enter ocasionally, but the writer cannot
enter.
• If there are some readers in the critical section by taking the lock, and there is a writer want to
enter. That writer has to wait if another reader is coming until all of readers have finish. That why
this mechanism is reader prefer.
Sequence lock conflict resolution the conflict resolution mechanism in teader-writer lock can cause
writer starvation. The following policy is implemented by the sequence lock:
• When no one is in the critical section, one writer can enter the critical section and takes the lock,
increasing the sequence number by one to an odd value. When the sequence number is an odd value,
the writing is happening. When the writing has been done, the sequence is back to even value. Only
one writer is allow into critical section.
• When the reader wants to read data, it checks the sequence number which is an odd value, then it
has to wait until the writer finish.
• When the value is even, many readers can enter the critical section to read the value.
• When there are only a reader and no writer in the critical section, if a writer want to enter the
critical section it can take the lock without blocking.
12
4 EXERCISE
Arguments :
The last argument < seednum > is an already implemented mechanism. This value is used to generate
the integer values in the array and we don’t touch it to keep it for later validated testcase generation.
The data generation mechanism is also provided. Call the following routine to fill in the ”buf” shared
memory buffer.
int g e n e r a t e a r r a y d a t a ( int ∗ buf , int a r r a y s i z e , int seednum ) ;
/∗
∗ TODO implement a t h r e a d s a f e sum o p e r a t o r works i n t h e range
∗ i . e . f o r ( i=i d x r a n g e . s t a r t ; i<= i d x r a n g e . end ; i ++){}
∗ and w r i t e t h e sum t o s u m b u f f ( i n program g l o b a l d a t a )
∗/
}
int main ( )
{
p t h r e a d t t i d ; /∗ Sample code i s o n l y s i n g l e t h r e a d ∗/
struct r a n g e t h r e a d i d x r a n g e ;
13
4 EXERCISE
...
/∗ Sample code use f u l l range ∗/
thread idx range . start = 0;
t h r e a d i d x r a n g e . end = a r r s z - 1 ;
...
/∗ TODO: implement m u l t i - t h r e a d mechanism ∗/
p t h r e a d c r e a t e (& t i d , NULL, sum worker , t h r e a d i d x r a n g e ) ) ;
}
In this problem, there are many programs or actors that call wrlog() to append the log data to a shared
buffer (i.e., log-file cache) which can be flushed to disk eventually. For simplicity, we assume the buffer
contains 5 (= M AX BU F F ER SLOT ) data slots and the flush event occurs periodically at time out.
We also assume a LOG is a fixed length string, i.e. char new log[M AX LOG LEN GT H].
In practical point of view, the behavior of the system can be illustrated as a sequence of writing log
data (wrlog()) and the flush (flushlog()) will be periodically triggered when a timeout is occured.
int main ( )
{
w r l o g ( data1 ) ;
w r l o g ( data2 ) ;
w r l o g ( data3 ) ;
...
w r l o g ( dataN ) ;
}
flushlog() clean buffer aka. move all the stored items to somewhere (in here is printing to screen) and then
delete all of them. This action runs eventually; for simplicy we just make a periodical call here.
Further development: The interruptable mechanism of flush log can (further) support more un-
predict events, i.e., it can handle a signall SIGU SR1, SIGU SR2 which are introduced in the previous
lab.
14
4 EXERCISE
You are provided a referenced code with non-protected buffer by default, the program’s output is:
$ ./ logbuf
flushlog ()
wrlog ( ) : 0
wrlog ( ) : 1
wrlog ( ) : 2
wrlog ( ) : 3
wrlog ( ) : 4
wrlog ( ) : 7
w r l o g ( ) : 12
w r l o g ( ) : 13
w r l o g ( ) : 16
w r l o g ( ) : 17
w r l o g ( ) : 21
w r l o g ( ) : 24
w r l o g ( ) : 11
w r l o g ( ) : 26
w r l o g ( ) : 14
wrlog ( ) : 6
w r l o g ( ) : 27
w r l o g ( ) : 28
wrlog ( ) : 8
w r l o g ( ) : 20
wrlog ( ) : 9
w r l o g ( ) : 22
w r l o g ( ) : 10
w r l o g ( ) : 19
w r l o g ( ) : 25
w r l o g ( ) : 29
w r l o g ( ) : 18
w r l o g ( ) : 23
wrlog ( ) : 5
w r l o g ( ) : 15
flushlog ()
Slot 0: 0
Slot 1: 1
Slot 2: 2
Slot 3: 3
Slot 4: 4
Slot 5: 7
S l o t 6 : 12
flushlog ()
flushlog ()
flushlog ()
15
4 EXERCISE
TODO: Implement the protection mechanism for wrlog() and flushlog() routines to make it a safe
data accessing (to buffer). If it has a proper configuration then the program behavior is somehow like
this illustration (comment out the print function name in wrlog() and flushlog() to make this clean
output).
$ ./ logbuf
Slot 0: 0
Slot 1: 1
Slot 2: 2
Slot 3: 3
Slot 4: 4
Slot 5: 5
S l o t 0 : 12
Slot 1: 6
Slot 2: 7
S l o t 3 : 10
Slot 4: 8
S l o t 5 : 11
S l o t 0 : 17
S l o t 1 : 13
S l o t 2 : 16
S l o t 3 : 15
S l o t 4 : 14
S l o t 5 : 18
S l o t 0 : 25
S l o t 1 : 20
S l o t 2 : 19
S l o t 3 : 21
S l o t 4 : 22
S l o t 5 : 23
S l o t 0 : 27
S l o t 1 : 28
S l o t 2 : 26
S l o t 3 : 24
S l o t 4 : 29
Slot 5: 9
16
Revision History 4 EXERCISE
Revision History
17