Understanding concurrency better (with locks)

Viewed 86

I'm trying to understand concurrency and using locks better, but this dummy example I made is throwing me off:

int i = 0;

void foo() {
    int n = i;
    i = i + 1;
    printf("foo: %d\n", n);
}

void boo() {
    int n = i;
    i = i + 1;
    printf("boo: %d\n", n);
}

int main(int argc, char* argv[]) {

    pthread_t p1, p2;

    pthread_create(&p1, NULL, (void*) foo, NULL);
    pthread_create(&p2, NULL, (void*) boo, NULL);

    // wait for threads to finish
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);

    // final print
    printf("main: %d\n", i);

    return 0;
}

If I understand correctly, the i = i + 1; in both foo() and bar() can cause some unexpected behaviour. One unexpected behaviour is that we'll get both "foo: 0" and "bar: 0" since it's possible that a context switch happened right before the i = i + 1; and so n is always 0. I think the expected behaviour is that "foo: 0" "bar: 1" or "bar: 0" "foo: 1" (please correct me if I'm wrong).

To fix this, I added locks:

int i = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void foo() {
    int n = i;
    i = i + 1;
    printf("foo: %d\n", n);
}

void boo() {
    int n = i;
    i = i + 1;
    printf("boo: %d\n", n);
}

int main(int argc, char* argv[]) {

    pthread_t p1, p2;

    printf("Locking foo\n");
    pthread_mutex_lock(&lock);
    printf("Locked foo\n");
    pthread_create(&p1, NULL, (void*) foo, NULL);
    pthread_mutex_unlock(&lock);
    printf("Unlocked foo\n");

    printf("Locking boo\n");
    pthread_mutex_lock(&lock);
    printf("Locked boo\n");
    pthread_create(&p2, NULL, (void*) boo, NULL);
    pthread_mutex_unlock(&lock);
    printf("Unlocked boo\n");

    // wait for threads to finish
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);

    // final print
    printf("main: %d\n", i);

    return 0;
}

I think this would fix the unexpected results, but I got a surprising output when I ran this:

Locking foo
Locked foo
Unlocked foo
Locking boo
Locked boo
foo: 0
Unlocked boo
boo: 1
main: 2

It looks like the program locked the first thread that calls foo() and then immediately unlocked it without actually executing the printf? It then goes on to locking the thread that calls boo() and does weird things out of order. Can someone explain this behaviour? I would of thought the output would have looked like:

Locking foo
Locked foo
foo: 0
Unlocked foo
Locking boo
Locked boo
boo: 1
Unlocked boo
main: 2
3 Answers

Your choice of wording betrays a likely serious misunderstanding:

It looks like the program locked the first thread that calls foo()

Programs do not lock threads. Rather, threads acquire locks (or, equivalently, threads lock mutexes). That can include a program's main thread, too. Mutual exclusion is achieved among cooperating (!) threads by the fact that only one thread can hold any particular lock (mutex) at a time.

Thus, if thread B has a given mutex locked when thread A attempts to acquire it then thread A's acquisition attempt will block (the return of the pthread_mutex_lock() call will be delayed). Thread A will not proceed until it does acquire the mutex. Thus, the boundaries of critical regions are defined by calls to pthread_mutex_lock() and pthread_mutex_unlock() on the same mutex. Approximately speaking, every participating thread must acquire the appropriate mutex before accessing shared shared variables, and each one must release the mutex when it is done to allow other threads to acquire it in turn.

Other answers have already presented details of how that might look in your example program.

You are using the locks incorrectly. You lock the mutex, start the thread, and unlock it. The thread runs without any knowledge of the locking operations. Use the lock in the functions sharing memory:

void foo() {
    pthread_mutex_lock(&lock);
    int n = i;
    i = i + 1;
    pthread_mutex_unlock(&lock);
    printf("foo: %d\n", n);
}

Do the same with boo function.

The locking should take place in the functions like here:

#include <stdio.h>
#include <pthread.h>

int i = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void foo() {
    printf("Locking foo\n");
    pthread_mutex_lock(&lock);
    printf("Locked foo\n");
    int n = i;
    i = i + 1;
    pthread_mutex_unlock(&lock);
    printf("Unlocked foo\n");
    printf("foo: %d\n", n);
}

void boo() {
    printf("Locking boo\n");
    pthread_mutex_lock(&lock);
    printf("Locked boo\n");
    int n = i;
    i = i + 1;
    pthread_mutex_unlock(&lock);
    printf("Unlocked boo\n");
    printf("boo: %d\n", n);
}

int main(int argc, char* argv[]) {

    pthread_t p1, p2;
    pthread_create(&p1, NULL, (void*) foo, NULL);
    pthread_create(&p2, NULL, (void*) boo, NULL);

    // wait for threads to finish
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);

    // final print
    printf("main: %d\n", i);

    return 0;
}

This way, when one function locks, the other function will be blocked until the lock is unlocked.

Related