1
votes

Story

According to the man page https://linux.die.net/man/3/pthread_mutex_lock

The mutex object referenced by mutex shall be locked by calling pthread_mutex_lock(). If the mutex is already locked, the calling thread shall block until the mutex becomes available.

I have a program with a thread. Here is the program flow:

  1. The main process and the thread always call pthread_mutex_lock inside of a loop.
  2. When the main process is holding the lock, the thread which is asking for lock, blocks (waiting for lock to be granted).
  3. When the main process releases the lock with pthread_mutex_unlock, the thread should suddenly get the lock.
  4. When the main process asks for the lock again, the main process should wait for the thread to release the lock.

The problem is that, at point 3, the thread does not suddenly get the lock as soon as the main process releases the lock. The main process gets it first when it calls to pthread_mutex_lock in the next loop cycle (at point 4).

How to deal with this situation?

Question

How can I make the thread get the lock as soon as the main process releases the lock?

Simple code to reproduce the problem

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

pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;

void *
my_thread(void *p)
{
  (void)p;

  while (1) {
    pthread_mutex_lock(&my_mutex);
    printf("The thread is holding the lock...\n");
    sleep(1);
    pthread_mutex_unlock(&my_mutex);
  }
}

int
main()
{
  pthread_t t;

  pthread_create(&t, NULL, my_thread, NULL);
  pthread_detach(t);

  while (1) {
    pthread_mutex_lock(&my_mutex);
    printf("The main process is holding the lock...\n");
    sleep(1);
    pthread_mutex_unlock(&my_mutex);
  }
}

Compile and Run

gcc test.c -o test -lpthread
./test

Expected result

The main process is holding the lock...
The thread is holding the lock...
The main process is holding the lock...
The thread is holding the lock...
The main process is holding the lock...
The thread is holding the lock...
The main process is holding the lock...
...

Actual result

The main process is holding the lock...
The main process is holding the lock...
The main process is holding the lock...
The main process is holding the lock...
The main process is holding the lock...
The main process is holding the lock...
The main process is holding the lock...
...

Calls story in order

main   -> [1] call lock (get the lock)
thread -> [2] call lock (waiting for main to unlock)
main   -> [3] call unlock
thread -> [4] (still does not get the lock from [2], why? even though it has been unlocked?)
main   -> [5] lock (get the lock again)
thread -> [6] (still does not get the lock from [2])
main   -> [7] call unlock
thread -> [8] (still does not get the lock from [2], why? even though it has been unlocked?)
main   -> [9] lock (get the lock again)
... and so on ...

Summary

pthread_mutex_lock does not guarantee the order of lock requests.

3
When the calling thread blocks waiting for the lock to be released, it's not guaranteed that it's actually going to acquire the lock, only that it's going to compete for it. After the main thread releases the lock, it immediately tries to re-acquire, and this appears to be fast enough that it's competing with the other thread, and lock acquisition is not guaranteed to be done in order. Adding a tiny delay in the main thread after release would probably give you the behavior you're looking for. - Steve Friedl

3 Answers

5
votes

pthread_mutex_lock guarantees that it will lock until the mutex becomes available. That does not mean that each lock() call enters a queue and is guaranteed to get the mutex lock next. It only means that nobody else will have the lock at the same time.

If you require a certain order, an option would be to use condition variables. That way, you can have a flag that is set to the next member which should get the mutex. You can then wait for the mutex until the value is as expected. See https://linux.die.net/man/3/pthread_cond_wait.

Alternatively, if your example has sleeps in it anyway as above, you can just move the sleep after the unlock() call. While that is not strictly speaking a guarantee, it will most definitely do the trick for a simple test. I do not recommend this approach for anything more serious/complex though.

EDIT: As Shawn correctly added, you can also use pthread_yield (1) to allow another thread to acquire the mutex if you don't care which other thread it is. Some intricacies with yielding are described in sched_yield(2).

PS: I would comment, but my rep is now high enough yet :)

1
votes

Here is a 'fairlock' as an example; you can do better:

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

typedef struct fairlock FairLock;
struct fairlock {
    pthread_mutex_t lock;
    pthread_cond_t  cv;
    long scount;
    long wcount;
};



#define err(x,v) do { int t; if ((t=(x)) != (v)) { \
    error(__FILE__,__LINE__, #x, t, (v)); \
}} while (0)

static void error(char *fn, int lno, char *s, long x, long v) {
    fprintf(stderr, "%s:%d %s returned %ld rather than %ld\n",
        fn, lno, s, x, v);
    exit(1);
}


void Lock(FairLock *f) {

    err(pthread_mutex_lock(&f->lock), 0);
    long me = f->scount++;
    while (f->wcount != me) {
        err(pthread_cond_wait(&f->cv, &f->lock), 0);
    }
    err(pthread_mutex_unlock(&f->lock), 0);

}

void UnLock(FairLock *f) {
    err(pthread_mutex_lock(&f->lock), 0);
    if (f->scount > f->wcount) {
        f->wcount++;
        err(pthread_cond_broadcast(&f->cv), 0);
    }
    err(pthread_mutex_unlock(&f->lock), 0);
}

FairLock *NewLock(void) {
    FairLock *p = malloc(sizeof *p);
    if (p != 0) {
        err(pthread_mutex_init(&p->lock, 0),0);
        err(pthread_cond_init(&p->cv, 0),0);
        p->scount = p->wcount = 0;
    }
    return p;
}

void DoneLock(FairLock *f) {
    err(pthread_mutex_destroy(&f->lock), 0);
    err(pthread_cond_destroy(&f->cv), 0);
}

And your testlock.c changed to utilize it; again there is room for improvement, but you should be able to stick sleeps just about anywhere and it will remain fair....

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

#include "fairlock.c"

FairLock *my;

void *
my_thread(void *p)
{
  while (1) {
    Lock(my);
    printf("%s is holding the lock...\n", p);
    UnLock(my);
  }
}



int
main()
{
  pthread_t t;
  my = NewLock();
  pthread_create(&t, NULL, my_thread, "one");
  pthread_detach(t);
  pthread_create(&t, NULL, my_thread, "two");
  pthread_detach(t);
  pthread_create(&t, NULL, my_thread, "three");
  pthread_detach(t);
  pthread_create(&t, NULL, my_thread, "four");
  pthread_detach(t);
  pthread_create(&t, NULL, my_thread, "five");
  pthread_detach(t);

  while (1) {
    Lock(my);
    printf("main process is holding the lock...\n");
    UnLock(my);
  }
}
1
votes

TLDR Version:

The very next thing that either of your two loops does after releasing the lock is to try to acquire it again.

When there's a race between thread A which has just released a lock and thread B which has been blocked, waiting for the lock, thread A almost always will win because thread A already is running, and thread B still is "asleep."

Releasing the lock doesn't instantaneously "wake up" the waiting thread. All it does is change the status of the other thread from "waiting for the lock" to "waiting to be assigned a CPU to run on." Some time real soon after, the Scheduler will get around to restoring the context of thread B on another CPU, and thread B will start running, but by then it will be too late. Thread A will have already re-locked the lock.