--- title: Ch6:Process Synchronization --- # 作業系統Ch6: Process Synchronization ###### tags: `Operating System` `Review` `Ch6` ## Background - <font color = red>Concurrent</font> access to 共享 data 導致 **data inconsistency** - 維護 data consistency 需要確保按順序地執行 cooperating processes 的機制。 ### Consumer & Producer Problem - 決定是否 buffer 是空的或滿的 - 過去: use <font color = red>in, out</font> position - 現在: use <font color = red>counter</font> ```c= /* producer */ while (1) { nextItem = getItem(); while (counter == BUFFER_SIZE); // buffer 滿了 buffer[in] = nextItem; in = (in + 1) % BUFFER_SIZE; counter++; // 生產一個,加一個 } /* consumer */ while (1) { while (counter == 0); // buffer 為空 item = buffer[out]; out = (out + 1) % BUFFER_SIZE; counter--; // 消耗一個,減一個 } ``` > 但是這樣會有 synchronization 問題,因為 counter++ 語句和 counter--語句在組語下指令可能是多行組成,如下: > ```c= > /* counter++ */ > move ax, counter > add ax, 1 > move counter, ax > /* counter-- */ > move bx, counter > sub bx, 1 > move counter, bx > ``` 用例子去看,如果一開始給定 $counter\ =\ 5$,過程可能會是如下這樣: ``` producer: move ax, counter //ax = 5 producer: add ax, 1 //ax = 6 context switch consumer: move bx, counter //bx = 5 consumer: sub bx, 1 //bx = 4 context switch producer: move counter, ax //counter = 6 context switch consumer: move counter, bx //counter = 4 ``` 最後這種情況的 $counter\ =\ 4$,而不是我們預期的$counter\ =\ 5$,甚至也可能產生 $5$ 和 $6$ 結果,取決於 runtime 時的情況。 ### Race Condition - 多個 processes 並行 (**concurrently**) 存取並操弄 shared data 的情況。 - shared data 的最終值會取決於process 最後完成的。 - 為了避免 race condition, concurrent processes 一定需要 synchronized - 在單一 processor 機器上,我們可以<font color =red> disable interrupt </font> or use <font color=red>non-preemptive CPU scheduling</font> > disable interrupt 會影響到在系統上不同的使用者,所以不會是一個好方法。 - 通常描述為 **critical section problem** ### Critical Section - 目標: 為 processes 協調的一套協議 (protocol) - 問題描述: - $N$ processes 為使用某個 shared data 而競爭 - 每一個 process 有一個稱作 **critical section** 的 code segment,shared data 在 critical section 被存取。 - 確保當一個 process 在它的 critical section 執行時,沒有其他 process 被允許進入在它的 critical section 執行。 $\rightarrow$ **mutually exclusive** - 一般 code section structure - Only one process can be in a critical section ```c= do { remainder section // entry section: Gets entry permission critical section // Modify shared data // exit section: Release entry permission remainder section } while (1); ``` #### Critical Section Requirements 1. Mutual Exclusion: 如果 process P 在 CS 中正在執行, 沒有其他 processes 可以在它們的 CS 中執行。 2. Progress: 如果沒有 process 在 CS 中正在執行,存在某些 processes 想要進入它們的 CS,這些 processes 不能被**無限期拖延**。 > 其中一個 process 要趕快進入 CS。 3. Bounded Waiting: 一個 process 做了要進入它的 CS 請求之後,允許其他 processes 進入其 CS 的次數必須存在限制。 > 換言之,已經請求的 process 不能「不確定地」無限等待。 #### Algorithm for Two Process - 只有兩個 processes, $P_{0}\ and\ P_{1}$ - Shared variable - `int turn;` //initially `turn = 0` - `turn = i` $\implies\ P_{i}\ can\ enter\ its\ critical\ section$ ```c= /* Process 0 */ do { while (turn != 0); // entry section critical section turn = 1; // exit section remainder section } while (1); /* Process 1 */ do { while (turn != 1); // entry section critical section turn = 0; // exit section remainder section } while (1); ``` 1. Mutual exclusion ? Yes,`turn` 只能是 `1 or 0` 2. Progress ? No,如果 Process 0 離開 CS ,很有可能 Process 1 還沒有意願要做 do-while,所以 Process 0 有意願要繼續進去 CS 時,會卡在 `while (turn != 0)`,不滿足在 CS 為空時,有意願進去的可以進去。 3. Bounded Waiting ? Yes, 因為是輪流進去,最多就是等一個 process。 ### Peterson’s Solution for Two Processes - Shared variables - `int turn;` //initially `turn = 0` - `turn = i` $\implies\ P_{i}\ can\ enter\ its\ critical\ section$ - `bool flag[2];` //initially `flag[0] = flag[1] = false;` - `flag[i] = true` $\implies\ P_{i}\ ready\ to\ enter\ its\ critical\ section$ ```c= // Pi: do { flag[i] = true; turn = j; while (flag[j] && turn == j); critical section // 進入 CS 當 (1) process 得到它的 turn //(2) 其他 process 沒有要進去 flag = false flag[i] = false; remainder section } while (1); ``` > 當 P0 做 do-while 時, 想要進去 CS,所以 `flag` 會設為 true,且 `turn` 這個許可權會先給對方(自己在內部 `while` 等待),如果 P1 已經在內部`while`等待時,對方會先進去,之後再把 `turn = 0` 許可權還給 P0;如果 P1 此時正在 remainder section 或者正要進入 `do-while` 的話,很快就會將`turn = 0` 許可權還給 P0。 1. Mutual exclusion ? Yes - if $P_{0}$ enter CS $\implies$ `flag[1] == false || turn == 0` - if $P_{1}$ enter CS $\implies$ `flag[0] == false || turn == 1` - 如果兩個 processes 都想進 CS $\implies$ `flag[0] = flag[1] = true` - `turn = 0` for $P_{0}$ to enter, turn = 1 for $P_{1}$ to enter > 然而,`turn` 只會是 `1 or 0` 因為它的值是設給兩個 processes,但是只有一個值能夠持續。 > 因此, $P_{0}$ 和 $P_{1}$ 不能同時待在 CS ![](https://i.imgur.com/fZEZ2OC.png) 2. Progress ? Yes $P_{0}$ 想要進去 CS - (1). if $P_{1}$ 還不想進去 $\implies$ `flag[1] = false` $\implies$ $P_{0}$ 可以進去 - (2). if both 都想進去 $\implies$ `flag[0] = flag[1] = true` - If `turn = 0` then $P_{0}$ enters, otherwise $P_{1}$ enters - 無論哪種情況,都有一些等待 process 可以進入 CS! ![](https://i.imgur.com/puDew9n.png) 3. Bounded waiting ? Yes $P_{0}$ 想要進去 CS - (1). 一旦 $P_{1}$ 離開 CS $\implies$ `flag[1] = false` $\implies$ $P_{0}$ 可以進去 - (2). 如果 $P_{1}$ 離開 CS 且重設 `flag[1] = true` $\implies$ `turn = 0` (overwrite $P_{0}$ setting) $\implies$ $P_{0}$ 可以進去 - $P_{0}$ 不會無止盡等待 ![](https://i.imgur.com/TUVFBpJ.png) ### Back To Producer/Consumer Problem ```c= /* producer */ while (1) { // entry-section(); (1) nextItem = getItem(); while (counter == BUFFER_SIZE); // buffer 滿了 buffer[in] = nextItem; in = (in + 1) % BUFFER_SIZE; // entry-section(); (2) entry-section(); //(3) counter++; // 生產一個,加一個 exit-section(); //~(3) computing(); //context switch // exit-section(); ~(2) // exit-section(); ~(1) } /* consumer */ while (1) { // entry-section(); (1) while (counter == 0); // buffer 為空 item = buffer[out]; out = (out + 1) % BUFFER_SIZE; // entry-section(); (2) entry-section(); //(3) counter--; // 消耗一個,減一個 exit-section(); //~(3) computing(); //context switch // exit-section(); ~(2) // exit-section(); ~(1) } ``` 1. 第一種情況會導致 deadlock, 如果 consumer 先進去 CS。 2. 第二種情況雖然正確,但是效能低落,因為會進入到 computing()。 3. 第三種情況正確且極大化 concurrent performance。 ### Bakery Algorithm (n processes) - 進入它的 CS 之前,每一個 process 會收到一個 number > 類似取餐號碼牌 - 數字最小的持有者可以進入 CS。 - 分發數字方案總是以非遞減順序產生數字,i.e., 1,2,**3,3**,4,**5,5,5**。 - 如果 processes $P_{i}\ and\ P_{j}$ 收到同樣數字,那麼如果 $i\ <\ j$,則 $P_{i}$ 會先進去。 - Notation: $(a,\ b)\ <\ (c,\ d)\ \ \ \ if\ a\ <\ c\ \ or\ \ if\ a\ =\ c\ \&\&\ b\ <\ d$ ```c= /* Process i: */ do { choosing[i] = true; //lock num[i] = max(num[0], num[1], ..., num[n - 1]) + 1; // Get tickets choosing[i] = false; //unlock for (j = 0; j < n; j++) { while (choosing[j]); // cannot compare when num is being modified while ((num[j] != 0) && ((num[j],j) < (num[i], i))); // FCFS } critical section num[i] = 0; // release ticket remainder section } while (1); ``` 1. Mutual exclusion ? Yes 只有數字最小的人能進去,如果有兩個或多個數字最小的,則看 Process 的編號(有唯一性),不會有兩個 processes 同時在它們自己的 CS。 2. Progress ? Yes 如果 CS 為空時,有一群 Process 拿到數字,其中拿到最小數字的 process (編號最小的) 一定可以 break 掉 for 迴圈裡的兩個 while,因此就可以進入 CS,不會無限期拖延(可以進去,但是卻沒辦法進去)。 3. Bounded-waiting ? Yes 因為 processes 進入 CS 是以 First come First serve,排隊進入,所以可以有限制的等待,而非不明確的等待。 - 是否有可能許多 processes 收到相同 number? 有可能,因為取號碼牌大致可以分成三個動作 1. 挑最大值 2. 進行加 1 3. 賦值給相對應的nums[i] | | $P_{i}$ | $P_{j}$ | | -------- | ----------------- | ----------------- | | $T_{0}$ | choosing[i]= true | | | $T_{1}$ | | choosing[j]= true | | $T_{2}$ | 挑選最大值並進行加1,未賦值 | | | $T_{3}$ | | 挑選最大值並進行加1,未賦值 | | $T_{4}$ | 賦值 | | | $T_{5}$ | | 賦值 | - 為什麼當 number 在修改時不能比較? - Without locking > 假如 5 是目前最大 number,如果 $P_{1}\ and\ P_{4}$ 一起要獲得數字,但是 $P_{4}$ 比 $P_{1}$ 早完成 ( 也就是,$P_{1}\ =\ 0,\ P_{4}\ =\ 6\ \rightarrow\ P_{4}\ will\ enter\ the\ CS$ ), 在 $P_{1}$ 拿到數字後, $P_{1}\ =\ P_{4}\ =\ 6$, $P_{1}$ 也將會進入 CS。 (實際上,$P_{1}\ 要比\ P_{4}\ 早進入才是正確的$) - With locking > $P_{4}$ 將必須等待到 $P_{1}$ 拿到數字,在比較之前,$P_{1}\ and\ P_{4}$ 都會拿到數字 6。 <br> ### Pthread Lock/Mutex Routines - 為了使用 mutex, 它必須宣告型態為 `pthread_mutex_t` 和以 `pthread_mutex_init()` 初始化。 - 以 `pthread_mutex_destroy()` 摧毀了一個 mutex。 - 一 critical section 可以使用 `pthread_mutex_lock()` 和 `pthread_mutex_unlock()` 受保護。 Example: ```c= #include <pthread.h> pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); //NULL: specify default attribute for the mutex pthread_mutex_lock(&mutex); //enter critical section critical section pthread_mutex_unlock(&mutex); //leave critical section pthread_mutex_destroy(&mutex); ``` prototype `int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);` 對於屬於允許 set or get,相關內容可見 [what is the "attribute" of a pthread mutex?](https://stackoverflow.com/questions/4252005/what-is-the-attribute-of-a-pthread-mutex)。值得一提的是 `NULL`作為預設屬性,且對於屬性中的 type 是 PTHREAD_MUTEX_DEFAULT,但是實際上這並沒有真的定義,可以是任何,所以需進行[測試](https://stackoverflow.com/questions/29624365/what-is-the-default-mutex-attributes-of-the-pthread-mutex)。 ### Condition Variables (CV) - CV 表示**某情況**一 thread 可以: - Wait on 直到 condition 發生,或 - 通知其他等待 threads, condition 已經發生了。 - 在 Condition Variables 上的三種操作: - <font color =red>`wait()`</font> --- <font color =red>Block</font> 直到另一 thread 呼叫 signal() or broadcast() on the CV. - <font color =red>`signal()`</font> --- 叫醒在 CV 上等待的一 thread。 - <font color =red>`broadcast()`</font> --- 叫醒在 CV 上等待的所有 thread。 - 在 Pthread 中, CV 型態是一 `pthread_cond_t` - 使用 `pthread_cond_init()` 初始化。 - `pthread_cond_wait(&theCV, &somelock)` > prototype: int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); - `pthread_cond_signal(&theCV)` > prototype: int pthread_cond_signal(pthread_cond_t *cond); - `pthread_cond_broadcast(&theCV)` > prototype: int pthread_cond_broadcast(pthread_cond_t *cond); Example: - 一 thread 可以設計成 **<font color =red>take action</font> 當 $x\ =\ 0$**。 - 另一 thread 有責任為 counter 遞減一。 - 所有 condition variable 操作 **<font color = red> 一定**執行於 mutex 已經有上了鎖</font>。 ``` c= #include <pthread.h> pthread_cond_t cond; pthread_cond_init(cond, NULL); pthread_mutex_t mutex; pthread_mutex_init(mutex, NULL); action() { pthread_mutex_lock(&mutex); while(x != 0) pthread_cond_wait(cond, mutex); /* some code here.... */ pthread_mutex_unlock(&mutex); take_action(); } counter() { pthread_mutex_lock(&mutex); x--; if (x == 0) pthread_cond_signal(cond); pthread_mutex_unlock(&mutex); } ``` > 如果 `action( )` 的 thread 先開始,前面已經 `mutex` 的物件會上鎖,接著因為 `x` 不等於 `0`,所以會進入 while 迴圈中,並執行 `pthread_cond_wait(cond, mutex)` 的 statement,這個 statement 會使 `action( )` 執行的 thread 進入 sleep 狀態, 並且 **releases the lock of action() thread**,然後這時的 `counter( )` 的 thread 進行 `mutex` 的上鎖 (與 `action ( )` 中 `mutex` 是同一物件),接著 `action( )` thread 會一直處於 sleep 狀態,而 `counter( )` thread 會持續對 `x` 的值進行減一,直到當 `x = 0`時,它會發送 `Signal( )` 喚醒 `action( )` 的 thread,但是此時 `counter` 的 thread 仍然是上鎖的,就是等到 `counter` 的 thread 執行 releases the lock,接著 `action( )` thread 會 **<font color = red>Re-acquire lock</font>**,恢復執行,然後`action( )` thread 才會 Release the lock。 ### ThreadPool implementation **Task structure :** ```c= typedef struct { void (*function)(void *); void *argument; } threadpool_task_t; ``` **Threadpool structure :** ```c= struct threadpool_t { pthread_mutex_t lock; pthread_cond_t notify; pthread_t *threads; threadpool_task_t *queue; int thread_count; int queue_size; int head; int tail; int count; int shutdown; //Flag indicating if the pool is shutting down int started; }; ``` **Allocate thread and task queue :** Threadpool 一開始建立的時候就決定 Thread Pool 和 tasks 的最大數量。 ```c= pool->threads = (pthread_t *) malloc(sizeof(pthread_t) * thread_count); pool->queue = (threadpool_task_t *) malloc(sizeof(threadpool_task_t) * queue_size); ``` **每個 Thread 要執行的 Function :** ```c= static void *threadpool_thread(void *threadpool) { threadpool_t *pool = (threadpool_t *)threadpool; threadpool_task_t task; for(;;) { /* Lock must be taken to wait on conditional variable */ pthread_mutex_lock(&(pool->lock)); /* Wait on condition variable, check for spurious wakeups. When returning from pthread_cond_wait(), we own the lock. */ while((pool->count == 0) && (!pool->shutdown)) { pthread_cond_wait(&(pool->notify), &(pool->lock)); } if((pool->shutdown == immediate_shutdown) || ((pool->shutdown == graceful_shutdown) && (pool->count == 0))) { break; } /* Grab our task */ task.function = pool->queue[pool->head].function; task.argument = pool->queue[pool->head].argument; pool->head += 1; pool->head = (pool->head == pool->queue_size) ? 0 : pool->head; pool->count -= 1; /* Unlock */ pthread_mutex_unlock(&(pool->lock)); /* Get to work */ (*(task.function))(task.argument); } pool->started--; pthread_mutex_unlock(&(pool->lock)); pthread_exit(NULL); return(NULL); } ``` > 在 `for(;;)` 裡面,Thread 第一件要做的事情就是去搶奪 pool 的 lock,當搶到 lock 的 Thread 發現沒有工作可以做的時候, 就會執行 `pthread_cond_wait` 來等待通知。這時候 `pool->lock` 會被 Unlock,因此這時候其他 Thread 也可以進來這個區域。 所以在完全沒有工作的情況下,所有的 Thread 都會在這邊 Waiting。 > 當 Thread 被透過 `pthread_cond_signal( )` 喚醒的時候,該 Thread 就會重新取得 `pool->lock`。 這時他就可以安心的取出 queue 中的 task,等待取出完畢之後,再 unlock 讓其他被喚醒的 Thread 也可以去取得 Task。 > 之後就是執行 task 中的 function pointer 做該做的工作。\[1]\[2] <br> ## Synchronization Hardware ### HW Support - HW support: **atomic instructions** - atomic: 作為一不可中斷的單位(硬體實作出來的) - examples: TestAndSet(var), Swap(a,b) $\rightarrow$ 假設的指令,實際上 Computer system 會有其他的指令。 ### Atomic TestAndSet() ```c= /** * execute atomically: return the value of "lock" and * set "lock" to TRUE. **/ bool TestAndSet(bool &lock) { bool value = lock; lock = true; return value; } ``` > 不管原先 lock 值是甚麼,就是上鎖,然後回傳原本 lock 的值。 Shared data: `bool lock;` //initially `lock = false;` ```c= /* P0 */ do { while (TestAndSet(lock)); //obtain lock critical section lock = false; remainder section // release lock } while (1); /* P1 */ do { while (TestAndSet(lock)); critical section lock = false; remainder section } while (1); ``` > $P_{0}$ 進入 do-while 會先遇到 while 的條件式 `TestAndSet( )`,由於一開始 `lock` 為 `false`,所以會 break the while 進入 critical section,此時 lock 會是 true,所以 $P_{1}$ 在 while 的條件式 `TestAndSet( )` 會回傳為 `true`,等待 $P_{0}$ 離開 critical section 並將 `lock` 設為 `false` ,這樣 $P_{1}$才會 break the while 進入 critical section。 1. Mutual exclusion ? Yes `lock` 只會為 `true` or `false`,當在一個 process 為 `false` 可以進去 critical section 時,其他 process 會因為 `TestAndSet` 將 lock 設為 true,而不得進去。 2. Progress ? Yes 當 critical section 沒有 process 時,`lock` 設為 `false`,只要有任何 process 去做 `while (TestAndSet (lock) ) ;` 都可以進去 critical section ,不會無限期 delay。 3. Bounded-Wait ? No 當 $P_{0}$ 離開 critical section 且`lock` 設為 `false`,有想要進去的其他 processes 並不是排隊進去,而是靠搶奪的方式誰先執行 `while (TestAndSet (lock) ) ;` ,誰就能先進去。 ### Atomic Swap() - idea: enter CS if `lock = false` Shared data: `bool lock;` //initially `lock = false;` ```c= /* P0 */ do { key0 = true; while (key0 == true) Swap(lock, key0); critical section lock = false; remainder section // release lock } while (1); /* P1 */ do { key1 = true; while (key1 == true) Swap(lock, key1); critical section lock = false; remainder section } while (1); ``` > 當 $P_{0}$ 的 `key0` 一開始為 `true` 並且 `lock = false`,進行 `swap` 交換,使 `key0` 為 false ,可以 break the while, 並且進入 critical section;其他 processes 的 `key1` 會由於 `swap` 交換到仍是`true`,所以卡在`while` 迴圈中,直到 $P_{0}$ 離開並將 `lock` 設為 `false`。 1. Mutual exclusion ? Yes 理由同 `TestAndSet`,`lock` 只會為 `true` or `false`,只允許一個 process 進入 critical section。 2. Progress ? Yes 理由同 `TestAndSet`。 3. Bounded-Wait ? No 理由同 `TestAndSet`,也是需要用搶奪的方式執行交換。 <br> ## Semaphores - 一般化 synchronization problem 的工具 - 更具體來說是「有多少可用的特定資源的 record」。 - if record # = 1 $\rightarrow$ <font color =red>binary semaphore, mutex lock</font> - if record # > 1 $\rightarrow$ <font color = red>counting semaphore</font> - 只存取透過 2 <font color = red>atomic</font> ops: **<font color = red>wait & signal</font>** - Spinlock implementation: > Spinlock: 對於 user 而言看起來沒有在動,但是對於 CPU 而言,卻是一直在重複跑 `while (S <= 0)`。 - Semaphore is an <font color = red>integer variable</font> #### busy waiting ```c= wait(S) { while (S <= 0); //busy waiting S--; } signal(S) { S++; } ``` > 浪費 CPU 資源 ### POSIX Semaphore - Semaphore 是 POSIX standard 的一部分但是不屬於`Pthread` - It can be used with or **<font color = red>without</font>** thread - POSIX Semaphore routines: - **sem_init(sem_t \*sem, int pshared, unsigned int value)** $\rightarrow$ Initial value of semaphore - **sem_wait(sem_t \*sem)** - **sem_post\(sem_t \*sem)** - **sem_getvalue(sem_t \*sem, int \*valptr)** $\rightarrow$ Current value of the semaphore - **sem_destory(sem_t \*sem)** - Example: ```c= #include<semaphore.h> sem_t sem; sem_init(&sem); sem_wait(&sem); critical section sem_post(&sem); sem_destory(&sem); ``` ### n-Process Critical Section Problem - Shared data: - semaphore mutex; //initially mutex = 1 Process $P_{i}$: ```c= do { wait(mutex); //pthread_mutex_lock(&mutex); critical section signal(mutex); //pthread_mutex_unlock(&mutex); remainder section } while (1); ``` 1. Mutual exclusion ? Yes 2. Progress ? Yes 3. Bounded waiting ? Yes It depends on the implementation `wait( )` ### Non-busy waiting Implementation - Semaphore 是 <font color = red>data struct with a queue</font> - 可以使用任何 queuing strategy (FIFO, FILO, etc) ```c= typedef struct { int value; //init to 0 struct process *L; //"PCB" queue } semaphore; ``` ![](https://i.imgur.com/2jNEwg2.png) - wait( ) and signal( ) - use system calls: <font color = red>sleep() and wakeup()</font> - 一定是 atomically 執行。 ```c= void wait(semaphore S) { S.value--; //subtract first if (S.value < 0) { add this process to S.L; sleep(); } } void signal(semaphore S) { S.value++; if (S.value <= 0) { remove a process P from S.L; wakeup(P); } } ``` - How to ensure atomic wait & signal ops? - Signal-processor: diable interrupts > 影響其他使用者 - Multi-processor: - HW support (e.g., TestAndSet, Swap) - SW solution (Peterson's solution, Bakery algorithm) ### Semaphore with Critical Section ```c= void wait (semaphore S) { entry-section(); S.value--; if (S.value < 0) { add this process to S.L; exit-section(); sleep(); } else { exit-section(); } } void signal (semaphore S) { entry-section(); S.value++; if (S.value <= 0) { remove a process P from S.L; exit-section(); wakeup(P); } else { exit-section(); } } ``` > Busy waiting for entry-section() ? > 僅限於 `wait` 和 `signal` 的 CS(~10 條 instructions) $\rightarrow$ very short period of time。Non-busy waiting 會牽扯到 system call ,所以指令會比 busy waiting 多很多,所以一般來說適合時間比較長的情況下,使用 Non-busy waiting, 而時間比較短的情況下用 Busy waiting。 System call 不用擔心 synchronization 問題,因為 OS 會去處理。 ### Cooperation Synchronization - P1 executes S1; P2 executes S2 - S2 只執行在 S1 完成後。 - Implementation - shared var: `semaphore sync; //initially sync = 0;` ![](https://i.imgur.com/iK8E3Ir.png) > 即使 P2 先執行也會卡住,等待 P1 執行完 S1 後,發 signal 去喚醒 P2。 #### A More Complicated Example 一開始,所有 semaphores 都是 $0$ ![](https://i.imgur.com/ediS0ny.png) > edge 代表著 semaphore,outcome 代表著 `signal`,而 income 代表著 `wait`。 ### Deadlocks & Starvation - Deadlocks: 2 processes 正在無止盡等待彼此釋出資源。 - Starvation: 例如: LIFO queue in semaphore process queue。 > 這是對於單一 process 來說。而 Deadlocks 則是對於多個 processes,但是同樣也會造成 starvation。 ![](https://i.imgur.com/vVZ57xp.png) > 如果 semaphore 的值都是 $0$,則 $P_{0}\ and\ P_{1}$ 會連第一個 `wait`都會卡住;如果 semaphore 的值都是 $1$,則 $P_{0}\ and\ P_{1}$ 通過第一個 `wait`,但是卡在第二個。全都是 Deadlock 的情況。 <br> ## Classical Problems of Synchronization 以經典 Synchronization 的問題,去用於測試新提出的同步方案。 - Bounded-Buffer (Producer-Consumer) Problem - Reader-Writers Problem - Dining-Philosopher Problem ### Bounded-Buffer Problem 同 Producer-Consumer 問題 ### Reader-Writers Problem - A set of shared data objects - A group of processes - reader processes (read shared objects) - writer processes (update shared objects) - a writer process 有獨佔存取 一個 shared object > 也就是在更新 shared object,不會給 reader 讀取。 - 牽扯到優先權的不同版本 - <font color = red>first RW problem</font>: 沒有 reader 需要等待,也就是現在有一個 reader 正在對於 a shared object,其他 reader 也可以進去讀取,除非現在是 writer 正在更新 a shared object。 > first RW 問題會有 writer 餓死情況發生。 - <font color = red>second RW problem</font>: 一旦 writer 準備好,只要 shared object 被 released,它馬上就會執行更新。 - writer has higher priority than reader - 一旦 writer 準備好,沒有新的 reader 可以開始 reading。 #### First Reader-Writer Algorithm - Readers 可以共享單一 wrt lock ```c= /* mutual exclusion for write */ semaphore wrt = 1; /* mutual exclusion for readcount */ semaphore mutex = 1; int readcount = 0; Writer() { while (true) { wait(wrt); // Writer Code signal(wrt); } } Reader() { while (true) { wait(mutex); readcount++; /* Acquire write lock if "reads" haven't */ if (readcount == 1) wait(wrt); signal(mutex); // Reader Code wait(mutex); readcount--; /* release write lock if no more reads */ if (readcount == 0) signal(wrt); signal(mutex); } } ``` > Writer 和 Reader 競爭 lock wrt,如果是 Reader 先 `wait(wrt)`,則 Writer 只能先進入 sleep,等待 Reader 的 `readcount = 0` (實際上是最後一個 reader)時發生 signal 喚醒 在 waiting queue 中的 writer,Reader 在這之前都會持續進行 Reader code 的執行,而 Writer code 的執行要等到收到 Signal 後才行。 ### Dining-Philosophers Problem - 5 個人坐在椅子上,桌上有 5 根筷子 - 一個人不是正在 thinking 就是正在 eating - thinking: 沒有與其他人有互動 > idle - eating: 需要兩根筷子在手 - 一個人一次「可能」只拿到一根筷子 $\rightarrow$ 可能身旁的人正在吃,所以他不能吃。 - 完成 eating: 放下手上兩根筷子 - deadlock problem - 一根筷子作為一個 semaphore - starvation problem - 有人吃不到 ![](https://i.imgur.com/LEiRSUi.png) > 解決方案,在接下來小節。 ## Monitors ### Motivation - 雖然 semaphores 提供了一個方便而且有效的 synchronization 機制,它的正確性卻是仰賴於 programmer - 所有 processes 存取一共享 data object 一定 execute `wait()` and `signal()` 在正確的順序和地方上。 - 這可能不會是成立,因為編程錯誤或不合作的 programmer #### Monitor --- A high-level language construct - a monitor 型態的表示組成由 - 變數的宣告,其值定義這型態的實例的狀態 (state)。 - **<font color = red>Procedures/functions</font>** 在類型上實作 operations。 - The monitor 類型類似於 <font color = red>class in O.O. language</font> - 在 monitor 內的 a procedure 只能存取 <font color = red>local variables</font> and the <font color = red>formal variables</font>。 - monitor 的 local variables 只可以被 local procedures 使用。 - 但是,monitor 確保<font color = red>一次只能有一個 process 在 monitor 內 **active**</font>。 ### Monitor - High-level synchronization construct 允許在 concurrent processes 之中安全共享 abstract data type。 ``` Syntax monitor monitor-name { //shared variable declarations procedure body P1(...){ ... } procedure body P2(...){ ... } procedure body Pn(...){ ... } initialization code{ } } ``` **Schematic View** <br> ![](https://i.imgur.com/mkNb6dx.png) ### Monitor Condition Variables - 為了允許一個 process <font color = red>wait **within**</font> the monitor, a condition variable 一定要宣告,如 - `condition x, y;` - Condition variable 只能與 operations `wait()` and `signal()` 一起使用 - `x.wait();` > 意思呼叫這個操作的 process 被暫停直到另一個 process invokes。 - `x.signal();` > 僅恢復一個暫停中的 process。 如果沒有 process 是暫停的,則 `signal` operation 沒有作用。 (相較之下,signal 總是改變 semaphore 的狀態) #### Monitor With Condition Variables ![](https://i.imgur.com/vpAIPdb.png) <br> ### Dining Philosophers Example ```c= monitor dp { enum {thinking, hungry, eating} state[5]; //current state condition self[5]; //delay eating if can't obtain chopsticks void pickup(int i); //pickup chopsticks void putdown(int i); //putdown chopsticks void test(int i); //try to eat void init() { for (int i = 0; i < 5; i++) state[i] = thinking; } } void pickup(int i) { state[i] = hungry; test[i]; //try to eat if (state[i] != eating) self[i].wait(); //wait to eat } void putdown(int i) { state[i] = thinking; /* checking if neighbors are waiting to eat */ test((i + 4) % 5); test((i + 1) % 5); } /* try to let Pi eat (if it it hungry) */ void test(int i) { if ((state[(i + 4) % 5] != eating) && (state[(i + 1) % 5] != eating) && (state[i] == hungry)) { /* No neighbors are eating and Pi is hungry */ state[i] = eating; /** * If Pi is suspended, resume it * If Pi is not suspended, no effect **/ self[i].signal(); } } ``` #### An illustration <br> ![](https://i.imgur.com/xLiEbLn.png) ``` P1: P2: DiningPhilosophers.pickup(1) DiningPhilosophers.pickup(2) eat eat DiningPhilosophers.putdown(1) DiningPhilosophers.putdown(2) ``` > P1 開始做 `pickup(1)`, 它的 `state[1] = hungry;`,接著做 `test(1)`,發現左右兩邊的人 `state` 都不是 eating,且自己 `state` 是 hungry,所以將自己 `state` 改成 `eating`,並且執行`self[1].signal` (no effect)。換 P2 開始做 `pickup(2)`,它的 `state[2] = hungry;`,接著做 `test(2)`,發現 `test(2)` if 條件不滿足,所以 `self[2].wait();`。P1 吃完後放下筷子 `DiningPhilosophers.putdown(1)`,它的 `state[1] = thinking;`,檢查 ( `test`)左右兩邊的人 (P2 and P0 `thinking`),其中 P2 的 `state` 會變成 `eating`,然後恢復暫停中的 P2, P2 可以去吃,然後 P2 吃完放下筷子`DiningPhilosophers.putdown(2)`。 ### Synchronized Tools in JAVA - Synchronized Methods (Monitor) - Synchronized method uses the method receiver as a lock - 在同一物件上,synchronized methods 的兩次呼叫不可能交錯(要等到一個做完,才能做另一個)。 ```java= public class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c--; } public synchronized int value() { return c; } } ``` - Synchronized Statement (Mutex Lock) - Synchronized blocks uses the <font color = red>expression</font> as a lock - A synchronized Statement: the thread 獲得的鎖只能夠用於在語句中已經提及到的 object or class > 下面的程式碼中,p1 前後會有 lock 和 unlock 去解決 synchronization problem - useful for improving concurrency with fine-grained ```java= public void run() { synchronized(p1) { int i = 10; // statement without locking requirement p1.display(s1); } } ``` ## Atomic Transactions ### System Model - Transaction: 執行單個邏輯功能的指令的集合。 - Atomic Transaction: operations happen as a single logical unit of work, in its entirely, or not at all。 > 在單一 logical 工作單元只有一次做完 transaction 或者根本不做。 - 對於 database system 而言,特別關注於 Atomic Transaction。 ### File I/O Example - Transaction 是 read 和 write 一連串的 operations。 - 由 `commit` (transaction successful) 或者由 `abort` (transaction failed) 來中止。 - Aborted transaction 一定要回滾到它未做任何改變的執行前。 ### Log-Based Recovery - 記錄到 stable storage 關於所有由 transaction (交易) 所修改的資訊 > stable storage: never lost its stored data - Write-ahead logging (預寫式日誌): 每個 log record 描述單一 transaction 寫入 operation。 - Transaction name - Data item name - Old & new values - Special events: $<T_{i},\ starts>,\ <T_{i},\ commits>$ - log 用於重建由 transactions 修改的 data items 的狀態。 - 使用 undo ($T_{i}$), redo($T_{i}$) to recover data ### Checkpoints - 當失敗發生時,必須查看 log 以確定必須重新執行哪些 transactions。 - 搜尋 process 是耗時的。 - 並非所有 transactions 都需要重做 - 使用 checkpoints 減少上述開銷 (overhead): - 輸出所有 log records 到 stable storage - 輸出所有 modified data 到 stable storage - 輸出一 log record \<checkpoint> 到 stable storage > 簡單來說,就是類似於將系統還原的還原點,不需要從 0 開始做,從有受到影響之前開始再做即可。 <br> ## Ref [1] [軟件開發平台及語言筆記大全(超詳細)](https://www.cntofu.com/book/46/index.html) [2] [threadpool/threadpool.c at master · mbrossard/threadpool · GitHub](https://github.com/mbrossard/threadpool/blob/master/src/threadpool.c) [3] [上課影片 link](https://www.youtube.com/playlist?list=PLS0SUwlYe8czigQPzgJTH2rJtwm0LXvDX) [4] [投影片 link](https://ocw.nthu.edu.tw/ocw/index.php?page=course_news_content&cid=141&id=999)