# Linux: Synchronization In modern computers, we have multi-core (multi-processor) to do parallel programming, however, synchronization between the threads is very hard to achieve. To deal with these sync issues, some solutions are provided. ## Basics ### Hardware Support #### 1. Atomic instruction Instruction that cannot be stopped while exectuting. - test and set - compare and swap - fecch and add #### 2. Memory Barrier Many compilers will optimize the code by **reordering** the instruction. Sometimes we don't want that, for example, we don't want some value be modified before the permission is granted. Memory barrier ensures **all load/store before the barrier are done before load/store after the barrier**. ```c= // thread 1 // ensure flag is loaded before x while (!flag) memory_barrier(); print(x); // thread2 // ensure x is modified before setting the flag x = 100; memory_barrier(); flag = 1; ``` ### Lock If a segment of code contains some **shared resources**, then it is called a **critical section**. Before entering the CS, each thread must acquire the lock. ```c= // thread 1, 2 might call this fn void fn() { acquirelock(lock); // CS x = 100; releaselock(lock); } ``` ### Deadlock If two processes are waiting to acquire the lock of each other, then a deadlock occurs. ```c= // thread1 acquirelock(lock1); // 1st acquirelock(lock2); // 3rd // thread2 acquirelock(lock2); // 2nd acquirelock(lock1); // 4th ``` ## xv6 xv6 provides 2 types of lock - spinlock - sleeplock ### spinlock By using - test and set - memory barrier to achieve locking. ```c= // Acquire the lock. // Loops (spins) until the lock is acquired. void acquire(struct spinlock *lk) { push_off(); // disable interrupts to avoid deadlock. if(holding(lk)) panic("acquire"); // Atomically acquire the flag while(__sync_lock_test_and_set(&lk->locked, 1) != 0) ; // Memory Barrier // we don't want load/store before the lock is actually acquired. __sync_synchronize(); // Record info about lock acquisition for holding() and debugging. lk->cpu = mycpu(); } // Release the lock. void release(struct spinlock *lk) { if(!holding(lk)) panic("release"); lk->cpu = 0; // Memory Barrier __sync_synchronize(); // Atomically set lk->locked = 0. __sync_lock_release(&lk->locked); pop_off(); } ``` However, this will decrease CPU performance, as it is just keeping asking if the lock is available, not completing the actual task. So here comes the second kind of lock. ### Sleeplock It is similar to **conditional variable**. We have a spinlock `lk` (call it **small lock** here) to protect the sleeplock `lock` (call it **big lock**). ```c= // Long-term locks for processes struct sleeplock { uint locked; // Is the lock held? struct spinlock lk; // spinlock protecting this sleep lock // For debugging: char *name; // Name of lock. int pid; // Process holding lock }; ``` The sleeplock works as follows 1. acquire the small `lk` 2. try to acquire the sleeplock 3. if sleeplock is not available: sleep and atomically release `lk` 4. if waked up: atomically acquire `lk` and go to 2. 5. release `lk` ```c= void acquiresleep(struct sleeplock *lk) { acquire(&lk->lk); while (lk->locked) { // atomically release the lock and be put to sleep queue sleep(lk, &lk->lk); } lk->locked = 1; lk->pid = myproc()->pid; release(&lk->lk); } void releasesleep(struct sleeplock *lk) { acquire(&lk->lk); lk->locked = 0; lk->pid = 0; // put a process waiting for this lock into ready queue wakeup(lk); // release the lock release(&lk->lk); } ``` If we just look at the code of sleeplock, we can discover an issue called **lost wakeup** problem. If process 1 releases the small lock and has not yet entered the sleep queue, meanwhile, process 2 acquires the small lock and calls `wakeup()` to found no one to wakeup, then process 1 sleeps and so it will never get up. In fact, it will not happen, as each process has another spinlock to protect its state. ```c= void sleep(void *chan, struct spinlock *lk) { struct proc *p = myproc(); // Must acquire p->lock in order to // change p->state and then call sched. // Once we hold p->lock, we can be // guaranteed that we won't miss any wakeup // (wakeup locks p->lock), // so it's okay to release lk. acquire(&p->lock); // prevents lost-wakeup release(lk); // Go to sleep. p->chan = chan; p->state = SLEEPING; sched(); // Tidy up. p->chan = 0; // Reacquire original lock. release(&p->lock); acquire(lk); } void wakeup(void *chan) { struct proc *p; for(p = proc; p < &proc[NPROC]; p++) { if(p != myproc()){ // must acquire process lock to read/write its state acquire(&p->lock); if(p->state == SLEEPING && p->chan == chan) { p->state = RUNNABLE; } release(&p->lock); } } } ``` ### conclusion Although sleeplock has a spinlock to protect it, processes won't wait much time spinning, as the spinlock just protects the acquisition of sleeplock, which is just a small CS ! In summary, **spinlock** is good for protecting **short critical section**, while **sleeplock** is good for **long critical section**. ###### tags: `Linux`