12/10/20 1. Threads and processes execute concurrently or in parallel and can **share resources** (like devices, memory, variables, data structures etc) 2. A process/thread can be interrupted at any point in time. The process "state" (including registers) is saved in the **process control block** The outcome of programs may be unpredictable: > Sharing data can lead to **inconsistencies**. > > The **outcome of execution** may **depend on the order** in which instructions are carried out. ```c #include #include int counter = 0; void * calc(void * number_of_increments) { int i; for(i = 0; i < *((int*) number_of_increments);i++) counter++; } int main() { int iterations = 50000000; pthread_t tid1,tid2; pthread_create(&tid1, NULL, calc, (void *) &iterations); pthread_create(&tid2, NULL, calc, (void *) &iterations); pthread_join(tid1,NULL); pthread_join(tid2,NULL); printf("The value of counter is: %d\n", counter); } ``` This piece of code creates two threads and points them towards the `calc` function. The `pthread_join(tid1,NULL);` line waits until thread 1 has finished before the code moves on. `counter++` consists of three separate actions. 1. *read* the value of counter from memory and **store it in a register** 2. *add* one to the value in the register 3. *store* the value of the register **in counter** in memory The above actions are **not** "atomic". This means they can be interrupted by the timer. TCB - *Thread Control Block* However the thread control block could be out of date by the time the thread starts running again. For example *counter* could be 2 but the thread control block still has the old value of *counter*. The problem is that simple instructions in C are actually multiple instructions in assembly code. Another example is `print()`. ```c void print() { chin = getchar(); chout = chin; putchar(chout); } ``` If the two threads are **interleaved** one after the other, there is no issue. ![img](assets/Q.png) However, if **interleaved** like this they do interact. The global variable used to store the character in thread 1, is overwritten when thread 2 runs. This means 1+1+1 = 2 ## Bounded Buffer > Consider a **bounded buffer** in which N items can be stored > > A **counter** is maintained to count the number of items currently in the buffer. It is **incremented** when something is added and **decremented** when an item is removed. > > Similar **concurrency problems** as with the calculation of sums happen in the bounded buffer which is a consumer problem. ```c // producer while (true) { //while buffer is full while (counter == BUFFER SIZE); /* do nothing */ // Produce item buffer[in] = new_item; in = (in + 1) % BUFFER_SIZE; counter++; } // consumer while (true) { // wait until items in buffer while (counter == 0); /* do nothing */ // Consume item consumed = buffer[out]; out = (out + 1) % BUFFER_SIZE; counter--; } ``` This is a circular queue: there's a start and end pointer (*in* and *out*). The shared counter is being manipulated from two different places, which can go wrong. ## Race Conditions A **race condition occurs** when multiple threads/processes **access shared data** and the result is dependent on **the order in which the instructions are interleaved**. ### Concurrency within the OS > **Kernels are pre-emptive** > **Multiple processes/threads are running** in the kernel. > Kernel processes can be **interrupted** at any point. > > The kernel maintains **data structures** > > 1. These data structures are accessed **concurrently.** > 2. These can be subject to **concurrency issues.** > > The OS must make sure that interactions within the OS do not result in race conditions. > > Processes **share resources** including memory, files, processor time, printers etc. > > The OS must: > > 1. provide **locking mechanisms** to implement **mutual exclusion** and **prevent starvation and deadlocks.** > 2. Allocate and deallocate these resources safely. A **critical section** is a set of instructions in which **shared resources** between processes/threads **are changed**. **Mutual exclusion** must be enforced for **critical sections**. > Only **one process at a time** should be in the critical section (mutual exclusion) > > Processes have to **get "permission"** before entering their critical section > > 1. Request a lock > 2. Hold the lock > 3. Release the lock Any solution to the **critical section problem** must satisfy the following requirements: 1. **Mutual exclusion** - only one process can be in its critical section at any one point in time. 2. **Progress** - any process must be able to enter its critical section at some point in time. (A process/thread has a right to enter its critical section at a point in time.) If there is no thread/process in the critical section, there is no reason for the current thread not to be allowed in the **critical section**. 3. **Fairness/bounded waiting** - fairly distributed waiting times/processes cannot be made to wait indefinitely. These requirements have to be satisfied independently of the order in which sequences are executed. ### Enforcing Mutual Exclusion **Approaches** for mutual exclusion can be: 1. Software based - Peterson's solution 2. Hardware based - `test_and_set()` `swap_and_comapare()` Deadlocks have to be prevented as well. #### Deadlock Example A set of processes/threads is *deadlocked* if each process/thread in the set is waiting for an event that only the other process/thread in the set can cause. Each **deadlocked process/thread** is waiting for a resource held by another deadlocked process/thread (which cannot run and hence release the resource). - Assume that X and Y are **mutually exclusive resources**. - Threads A and B need to **acquire both resources** and request them in opposite orders. **Four conditions** must hold for a deadlock to occur 1. **Mutual exclusion** - a resource can be assigned to at most one process at a time. 2. **Hold and wait condition** - a resource can be held while requesting new resources. 3. **No pre-emption** - resources cannot be forcefully taken away from a process 4. **Circular wait** - there is a circular chain of two or more processes waiting for a resource held by the other processes. **No deadlocks** can occur if one of the conditions isn't met.