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 Stevens suggests“Advanced Programming in the UNIX Environment” by W. Richard Stevens and Stephen A. Rago. Addison-Wesley, 2005. As we’ve said many times, buy this book, and read it, in little chunks, preferably before going to bed. This way, you will actually fall asleep more quickly; more importantly, you learn a little more about how to become a serious UNIX programmer., we wrap the thread creation and join routines to simply exit on failure; for a program as simple as this one, we want to at least notice an error occurred (if it did), but not do anything very smart about it (e.g., just exit). Thus,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:

Press + to interact
prompt> gcc -o t1 t1.c -Wall -pthread; ./t1 20000000
main: begin (counter = 0)
A: begin
B: begin
A: done
B: done
main: 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:

Press + to interact
prompt> ./t1
main: begin (counter = 0)
A: begin
B: begin
A: done
B: done
main: 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)

Press + to interact
prompt> ./t1
main: begin (counter = 0)
A: begin
B: begin
A: done
B: done
main: 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.