Why It Gets Worse: Shared Data
In this lesson, you will see how sharing data between threads can become problematic.
We'll cover the following
The simple thread example you saw in the last lesson was useful in showing how threads are created and how they can run in different orders depending on how the scheduler decides to run them. What it doesn’t show you, though, is how threads interact when they access shared data.
Interaction of threads
Let’s imagine a simple example where two threads wish to update a global shared variable. The code you’ll study is in the coding widget below.
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include "common.h" #include "common_threads.h" int max; volatile int counter = 0; // shared global variable void *mythread(void *arg) { char *letter = arg; int i; // stack (private per thread) printf("%s: begin [addr of i: %p]\n", letter, &i); for (i = 0; i < max; i++) { counter = counter + 1; // shared: only one } printf("%s: done\n", letter); return NULL; } int main(int argc, char *argv[]) { if (argc != 2) { fprintf(stderr, "usage: main-first <loopcount>\n"); exit(1); } max = atoi(argv[1]); pthread_t p1, p2; printf("main: begin [counter = %d] [%x]\n", counter, (unsigned int) &counter); Pthread_create(&p1, NULL, mythread, "A"); Pthread_create(&p2, NULL, mythread, "B"); // join waits for the threads to finish Pthread_join(p1, NULL); Pthread_join(p2, NULL); printf("main: done\n [counter: %d]\n [should: %d]\n", counter, max*2); return 0; }
Here are a few notes about the code. First, as Pthread_create()
simply calls pthread_create()
and makes sure the return code is 0; if it isn’t, Pthread_create()
just prints a message and exits.
Second, instead of using two separate function bodies for the worker threads, we just use a single piece of code and pass the thread an argument (in this case, a string) so we can have each thread print a different letter before its messages.
Finally, and most importantly, we can now look at what each worker is trying to do: add a number to the shared variable counter
, and do so 10 million times (1e7) in a loop. Thus, the desired final result is 20,000,000.
You now compile and run the program, to see how it behaves. Sometimes, everything works as it is expected:
prompt> gcc -o t1 t1.c -Wall -pthread; ./t1 20000000main: begin (counter = 0)A: beginB: beginA: doneB: donemain: done with both (counter = 20000000)
Unfortunately, when we run this code, even on a single processor, we don’t necessarily get the desired result. Sometimes, we get:
prompt> ./t1main: begin (counter = 0)A: beginB: beginA: doneB: donemain: done with both (counter = 19345221)
Let’s try it one more time, just to see if we’ve gone crazy. After all, aren’t computers supposed to produce deterministic results, as you have been taught?! Perhaps your professors have been lying to you? (gasp)
prompt> ./t1main: begin (counter = 0)A: beginB: beginA: doneB: donemain: done with both (counter = 19221041)
Not only is each run wrong, but also yields a different result! A big question remains: why does this happen?
Get hands-on with 1400+ tech skills courses.