Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does this implementation of mutex locks result in undefined behavior?

I need to control the frequency at which main processes data. In the example, it just increases the value of a variable. I cannot use sleep inside of main because I need the frequency to be constant (and I don't know exactly how long does it take to process all the data). I just know for a fact that whatever processing I need to do takes less than 2 seconds, so I just need to prevent main from increasing x more than once every two seconds.

The solution I've found involves using two mutexes: locking one in main and unlocking it in an extra thread, and locking the other in extra and unlocking it in main. This extra thread sleeps for 2 seconds per cycle.

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

void  *extra(void *arg)
{
    pthread_mutex_t *lock = (pthread_mutex_t *) arg;
    while(1) {
        pthread_mutex_unlock(&lock[0]);
        pthread_mutex_lock(&lock[1]);
        sleep(2);
    }
}

int main()
{
    int x = 0;

    pthread_mutex_t lock[2];
    pthread_mutex_init(&lock[0], NULL);
    pthread_mutex_init(&lock[1], NULL);

    pthread_mutex_lock(&lock[1]);

    pthread_t extra_thread;
    pthread_create(&extra_thread, NULL, &extra, lock);

    while(1) {
        x += 1;
        printf("%d\n", x);

        pthread_mutex_lock(&lock[0]);
        pthread_mutex_unlock(&lock[1]);
    }
}

The Problem

The reason why this works is that main cannot lock lock[0] twice; it has to wait until extra unlocks it. However, according to The Open Group

Attempting to relock the mutex causes deadlock. If a thread attempts to unlock a mutex that it has not locked or a mutex which is unlocked, undefined behavior results.

The Question

Based on this, I see two issues here:

  1. If main tries to lock lock[0] twice it should deadlock.
  2. extra unlocking lock[0], which was locked by main, should be undefined behavior.

Is my analysis correct?

like image 489
David Avatar asked May 08 '19 02:05

David


People also ask

What happens when mutex is locked?

Mutexes are used to protect shared resources. If the mutex is already locked by another thread, the thread waits for the mutex to become available. The thread that has locked a mutex becomes its current owner and remains the owner until the same thread has unlocked it.

What happens when mutex lock is used more than once?

Deadlock! If a thread which had already locked a mutex, tries to lock the mutex again, it will enter into waiting list of that mutex which results in deadlock.

Can a mutex cause a deadlock?

Mutexes are used to prevent multiple threads from causing a data race by accessing the same shared resource at the same time. Sometimes, when locking mutexes, multiple threads hold each other's lock, and the program consequently deadlocks.

What state could an application Enter if a mutex is destroyed while it is locked?

Destroying a mutex while it is locked may result in invalid control flow and data corruption.


1 Answers

Answering your questions,

  1. If main tries to lock lock[0] twice it should deadlock.

Yes, it would. Unless you use recursive mutexes, but then your child thread would never be able to lock the mutex as main would always have it locked.

  1. extra unlocking lock[0], which was locked by main, should be undefined behavior.

Per the POSIX documentation for pthread_mutex_unlock(), this is undefined behavior for a NORMAL and non-robust mutex. However, the DEFAULT mutex does not have to be NORMAL and non-robust so there is this caveat:

If the mutex type is PTHREAD_MUTEX_DEFAULT, the behavior of pthread_mutex_lock() [and pthread_mutex_unlock()] may correspond to one of the three other standard mutex types as described in the table above. If it does not correspond to one of those three, the behavior is undefined for the cases marked.

(Note my addition of pthread_mutex_unlock(). The table of mutex behavior clearly shows that unlock behavior for a non-owner varies between different types of mutexes and even uses the same "dagger" mark in the "Unlock When Not Owner" column as used in the "Relock" column, and the "dagger" mark refers to the footnote I quoted.)

A robust NORMAL, ERRORCHECK, or RECURSIVE mutex will return an error if a non-owning thread attempts to unlock it, and the mutex remains locked.

A simpler solution is to use a pair of semaphores (the following code is deliberately missing error checking along with empty lines that would otherwise increase readability in order to eliminate/reduce any vertical scroll bar):

#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
sem_t main_sem;
sem_t child_sem;
void *child( void *arg )
{
    for ( ;; )
    {
        sem_wait( &child_sem );
        sleep( 2 );
        sem_post( &main_sem );
    }
    return( NULL );
}
int main( int argc, char **argv )
{
    pthread_t child_tid;
    sem_init( &main_sem, 0, 0 );
    sem_init( &child_sem, 0, 0 );
    pthread_create( &child_tid, NULL, child, NULL );
    int x = 0;
    for ( ;; )
    {
        // tell the child thread to go
        sem_post( &child_sem );
        // wait for the child thread to finish one iteration
        sem_wait( &main_sem );
        x++;
        printf("%d\n", x);
    }
    pthread_join( child_tid, NULL );
}
like image 146
Andrew Henle Avatar answered Sep 30 '22 18:09

Andrew Henle