================================================================ PART 1 ================================================================ 1. Example to illustrate interleavings: say that thread A executes f() and thread B executes g(). (Here, we are using the term "thread" abstractly. This example applies to any of the approaches that fall under the word "thread".) a. [this is pseudocode] int x; int main(int argc, char** argv) { tid tid1 = thread_create(f, NULL); tid tid2 = thread_create(g, NULL); thread_join(tid1); thread_join(tid2); printf("%d\n", x); } void f() { x = 1; thread_exit(); } void g() { x = 2; thread_exit(); } What are possible values of x after A has executed f() and B has executed g()? In other words, what are possible outputs of the program above? b. Same question as above, but f() and g() are now defined as follows int y = 12; f() { x = y + 1; } g() { y = y * 2; } What are the possible values of x? c. Same question as above, but f() and g() are now defined as follows: int x = 0; f() { x = x + 1; } g() { x = x + 2; } What are the possible values of x? 2. Linked list example struct List_elem { int data; struct List_elem* next; }; List_elem* head = 0; insert(int data) { List_elem* l = new List_elem; l−>data = data; l−>next = head; head = l; } What happens if two threads execute insert() at once and we get the following interleaving? thread 1: l->next = head thread 2: l->next = head thread 2: head = l; thread 1: head = l; 3. Producer/consumer example: /* "buffer" stores BUFFER_SIZE items "count" is number of used slots. a variable that lives in memory "out" is next empty buffer slot to fill (if any) "in" is oldest filled slot to consume (if any) */ void producer (void *ignored) { for (;;) { /* next line produces an item and puts it in nextProduced */ nextProduced = means_of_production(); while (count == BUFFER_SIZE) ; // do nothing buffer [in] = nextProduced; in = (in + 1) % BUFFER_SIZE; count++; } } void consumer (void *ignored) { for (;;) { while (count == 0) ; // do nothing nextConsumed = buffer[out]; out = (out + 1) % BUFFER_SIZE; count−−; /* next line abstractly consumes the item */ consume_item(nextConsumed); } } /* what count++ probably compiles to: reg1 <−− count # load reg1 <−− reg1 + 1 # increment register count <−− reg1 # store what count−− could compile to: reg2 <−− count # load reg2 <−− reg2 − 1 # decrement register count <−− reg2 # store */ What happens if we get the following interleaving? reg1 <−− count reg1 <−− reg1 + 1 reg2 <−− count reg2 <−− reg2 − 1 count <−− reg1 count <−− reg2 ================================================================ PART 2 ================================================================ The examples above depict cases of race conditions. This part demonstrates the use of concurrency primitives (mutexes, etc.) to protect critical sections and eliminate race conditions. 1. Protecting the linked list...... Mutex list_mutex; insert(int data) { List_elem* l = new List_elem; l−>data = data; acquire(&list_mutex); l−>next = head; head = l; release(&list_mutex) 2. Produce/consume revisited (also known as bounded buffer) Mutex mutex; void producer (void *ignored) { for (;;) { /* next line produces an item and puts it in nextProduced */ nextProduced = means_of_production(); acquire(&mutex); while (count == BUFFER_SIZE) { release(&mutex); yield(); /* or schedule() */ acquire(&mutex); } buffer [in] = nextProduced; in = (in + 1) % BUFFER_SIZE; count++; release(&mutex); } } void consumer (void *ignored) { for (;;) { acquire(&mutex); while (count == 0) { release(&mutex); yield(); /* or schedule() */ acquire(&mutex); } nextConsumed = buffer[out]; out = (out + 1) % BUFFER_SIZE; count−−; release(&mutex); /* next line abstractly consumes the item */ consume_item(nextConsumed); } } ===============================================================