Using mutexes

A mutex is a mutual exclusion lock. Only one thread can hold the lock.

Mutexes are used to protect data or other resources from concurrent access. A mutex has attributes, which specify the characteristics of the mutex.

Mutex attributes object

Like threads, mutexes are created with the help of an attributes object. The mutex attributes object is an abstract object, containing several attributes, depending on the implementation of POSIX options. It is accessed through a variable of type pthread_mutexattr_t. In AIX®, the pthread_mutexattr_t data type is a pointer; on other systems, it may be a structure or another data type.

Creating and destroying the mutex attributes object

The mutex attributes object is initialized to default values by the pthread_mutexattr_init subroutine. The attributes are handled by subroutines. The thread attributes object is destroyed by the pthread_mutexattr_destroy subroutine. This subroutine may release storage dynamically allocated by the pthread_mutexattr_init subroutine, depending on the implementation of the threads library.

In the following example, a mutex attributes object is created and initialized with default values, then used and finally destroyed:
pthread_mutexattr_t attributes;
                /* the attributes object is created */
...
if (!pthread_mutexattr_init(&attributes)) {
                /* the attributes object is initialized */
        ...
                /* using the attributes object */
        ...
        pthread_mutexattr_destroy(&attributes);
                /* the attributes object is destroyed */
}

The same attributes object can be used to create several mutexes. It can also be modified between mutex creations. When the mutexes are created, the attributes object can be destroyed without affecting the mutexes created with it.

Mutex attributes

The following mutex attributes are defined:

Attribute Description
Protocol Specifies the protocol used to prevent priority inversions for a mutex. This attribute depends on either the priority inheritance or the priority protection POSIX option.
Process-shared Specifies the process sharing of a mutex. This attribute depends on the process sharing POSIX option.

For more information on these attributes, see Threads Library Options and Synchronization Scheduling.

Creating and destroying mutexes

A mutex is created by calling the pthread_mutex_init subroutine. You may specify a mutex attributes object. If you specify a NULL pointer, the mutex will have the default attributes. Thus, the following code fragment:
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
...
pthread_mutexattr_init(&attr);
pthread_mutex_init(&mutex, &attr);
pthread_mutexattr_destroy(&attr);
is equivalent to the following:
pthread_mutex_t mutex;
...
pthread_mutex_init(&mutex, NULL);

The ID of the created mutex is returned to the calling thread through the mutex parameter. The mutex ID is an opaque object; its type is pthread_mutex_t. In AIX, the pthread_mutex_t data type is a structure; on other systems, it might be a pointer or another data type.

A mutex must be created once. However, avoid calling the pthread_mutex_init subroutine more than once with the same mutex parameter (for example, in two threads concurrently executing the same code). Ensuring the uniqueness of a mutex creation can be done in the following ways:

  • Calling the pthread_mutex_init subroutine prior to the creation of other threads that will use this mutex; for example, in the initial thread.
  • Calling the pthread_mutex_init subroutine within a one time initialization routine. For more information, see One-Time Initializations.
  • Using a static mutex initialized by the PTHREAD_MUTEX_INITIALIZER static initialization macro; the mutex will have default attributes.
After the mutex is no longer needed, destroy it by calling the pthread_mutex_destroy subroutine. This subroutine may reclaim any storage allocated by the pthread_mutex_init subroutine. After having destroyed a mutex, the same pthread_mutex_t variable can be reused to create another mutex. For example, the following code fragment is valid, although not very practical:
pthread_mutex_t mutex;
...
for (i = 0; i < 10; i++) {
 
        /* creates a mutex */
        pthread_mutex_init(&mutex, NULL);
 
        /* uses the mutex */
 
        /* destroys the mutex */
        pthread_mutex_destroy(&mutex);
}

Like any system resource that can be shared among threads, a mutex allocated on a thread's stack must be destroyed before the thread is terminated. The threads library maintains a linked list of mutexes. Thus, if the stack where a mutex is allocated is freed, the list will be corrupted.

Types of mutexes

The type of mutex determines how the mutex behaves when it is operated on. The following types of mutexes exist:
PTHREAD_MUTEX_DEFAULT or PTHREAD_MUTEX_NORMAL
Results in a deadlock if the same pthread tries to lock it a second time using the pthread_mutex_lock subroutine without first unlocking it. This is the default type.
PTHREAD_MUTEX_ERRORCHECK
Avoids deadlocks by returning a non-zero value if the same thread attempts to lock the same mutex more than once without first unlocking the mutex.
PTHREAD_MUTEX_RECURSIVE
Allows the same pthread to recursively lock the mutex using the pthread_mutex_lock subroutine without resulting in a deadlock or getting a non-zero return value from pthread_mutex_lock. The same pthread has to call the pthread_mutex_unlock subroutine the same number of times as it called pthread_mutex_lock subroutine in order to unlock the mutex for other pthreads to use.

When a mutex attribute is first created, it has a default type of PTHREAD_MUTEX_NORMAL. After creating the mutex, the type can be changed using the pthread_mutexattr_settype API library call.

The following is an example of creating and using a recursive mutex type:
pthread_mutexattr_t    attr;
pthread_mutex_t         mutex;

pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);

struct {
        int a;
        int b;
        int c;
} A;

f()
{
        pthread_mutex_lock(&mutex);
        A.a++;
        g();
        A.c = 0;
        pthread_mutex_unlock(&mutex);
}

g()
{
        pthread_mutex_lock(&mutex);
        A.b += A.a;
        pthread_mutex_unlock(&mutex);
}

Locking and unlocking mutexes

A mutex is a simple lock, having two states: locked and unlocked. When it is created, a mutex is unlocked. The pthread_mutex_lock subroutine locks the specified mutex under the following conditions:

  • If the mutex is unlocked, the subroutine locks it.
  • If the mutex is already locked by another thread, the subroutine blocks the calling thread until the mutex is unlocked.
  • If the mutex is already locked by the calling thread, the subroutine might block forever or return an error depending on the type of mutex.

The pthread_mutex_trylock subroutine acts like the pthread_mutex_lock subroutine without blocking the calling thread under the following conditions:

  • If the mutex is unlocked, the subroutine locks it.
  • If the mutex is already locked by any thread, the subroutine returns an error.

The thread that locked a mutex is often called the owner of the mutex.

The pthread_mutex_unlock subroutine resets the specified mutex to the unlocked state if it is owned by the calling mutex under the following conditions:

  • If the mutex was already unlocked, the subroutine returns an error.
  • If the mutex was owned by the calling thread, the subroutine unlocks the mutex.
  • If the mutex was owned by another thread, the subroutine might return an error or unlock the mutex depending on the type of mutex. Unlocking the mutex is not recommended because mutexes are usually locked and unlocked by the same pthread.

Because locking does not provide a cancelation point, a thread blocked while waiting for a mutex cannot be canceled. Therefore, it is recommended that you use mutexes only for short periods of time, as in instances where you are protecting data from concurrent access. For more information, see Cancelation Points and Canceling a Thread.

Protecting data with mutexes

Mutexes are intended to serve either as a low-level primitive from which other thread synchronization functions can be built or as a data protection lock. For more information about implementing long locks and writer-priority readers/writers locks see Using mutexes.

Mutex usage example

Mutexes can be used to protect data from concurrent access. For example, a database application may create several threads to handle several requests concurrently. The database itself is protected by a mutex called db_mutex. For example:
/* the initial thread */
pthread_mutex_t mutex;
int i;
...
pthread_mutex_init(&mutex, NULL);    /* creates the mutex      */
for (i = 0; i < num_req; i++)        /* loop to create threads */
        pthread_create(th + i, NULL, rtn, &mutex);
...                                  /* waits end of session   */
pthread_mutex_destroy(&mutex);       /* destroys the mutex     */
...

/* the request handling thread */
...                                  /* waits for a request  */
pthread_mutex_lock(&db_mutex);       /* locks the database   */
...                                  /* handles the request  */
pthread_mutex_unlock(&db_mutex);     /* unlocks the database */
...

The initial thread creates the mutex and all the request-handling threads. The mutex is passed to the thread using the parameter of the thread's entry point routine. In a real program, the address of the mutex may be a field of a more complex data structure passed to the created thread.

Avoiding Deadlocks

There are a number of ways that a multithreaded application can deadlock. Following are some examples:
  • A mutex created with the default type, PTHREAD_MUTEX_NORMAL, cannot be relocked by the same pthread without resulting in a deadlock.
  • An application can deadlock when locking mutexes in reverse order. For example, the following code fragment can produce a deadlock between threads A and B.
    /* Thread A */
    pthread_mutex_lock(&mutex1);
    pthread_mutex_lock(&mutex2);
    
    /* Thread B */
    pthread_mutex_lock(&mutex2);
    pthread_mutex_lock(&mutex1);
  • An application can deadlock in what is called resource deadlock. For example:
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } A;
    
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } B;
    
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } C;
    
    use_all_buffers()
    {
            pthread_mutex_lock(&A.mutex);
            /* use buffer A */
    
            pthread_mutex_lock(&B.mutex);
            /* use buffers B */
    
            pthread_mutex_lock(&C.mutex);
            /* use buffer C */
    
            /* All done */
            pthread_mutex_unlock(&C.mutex);
            pthread_mutex_unlock(&B.mutex);
            pthread_mutex_unlock(&A.mutex);
    }
    
    use_buffer_a()
    {
            pthread_mutex_lock(&A.mutex);
            /* use buffer A */
            pthread_mutex_unlock(&A.mutex);
    }
    
    functionB()
    {
            pthread_mutex_lock(&B.mutex);
            /* use buffer B */
            if (..some condition)
            { 
              use_buffer_a();
            }
            pthread_mutex_unlock(&B.mutex);
    }
    
    /* Thread A */
    use_all_buffers();
    
    /* Thread B */
    functionB();
    This application has two threads, thread A and thread B. Thread B starts to run first, then thread A starts shortly thereafter. If thread A executes use_all_buffers() and successfully locks A.mutex, it will then block when it tries to lock B.mutex, because thread B has already locked it. While thread B executes functionB and some_condition occurs while thread A is blocked, thread B will now also block trying to acquire A.mutex, which is already locked by thread A. This results in a deadlock.

    The solution to this deadlock is for each thread to acquire all the resource locks that it needs before using the resources. If it cannot acquire the locks, it must release them and start again.

Mutexes and race conditions

Mutual exclusion locks (mutexes) can prevent data inconsistencies due to race conditions. A race condition often occurs when two or more threads must perform operations on the same memory area, but the results of computations depends on the order in which these operations are performed.

Consider, for example, a single counter, X, that is incremented by two threads, A and B. If X is originally 1, then by the time threads A and B increment the counter, X should be 3. Both threads are independent entities and have no synchronization between them. Although the C statement X++ looks simple enough to be atomic, the generated assembly code may not be, as shown in the following pseudo-assembler code:
move    X, REG
inc     REG
move    REG, X

If both threads in the previous example are executed concurrently on two CPUs, or if the scheduling makes the threads alternatively execute on each instruction, the following steps may occur:

  1. Thread A executes the first instruction and puts X, which is 1, into the thread A register. Then thread B executes and puts X, which is 1, into the thread B register. The following example illustrates the resulting registers and the contents of memory X.
    Thread A Register = 1
    Thread B Register = 1
    Memory X          = 1
  2. Thread A executes the second instruction and increments the content of its register to 2. Then thread B increments its register to 2. Nothing is moved to memory X, so memory X stays the same. The following example illustrates the resulting registers and the contents of memory X.
    Thread A Register = 2
    Thread B Register = 2
    Memory X          = 1
  3. Thread A moves the content of its register, which is now 2, into memory X. Then thread B moves the content of its register, which is also 2, into memory X, overwriting thread A's value. The following example illustrates the resulting registers and the contents of memory X.
    Thread A Register = 2
    Thread B Register = 2
    Memory X          = 2

In most cases, thread A and thread B execute the three instructions one after the other, and the result would be 3, as expected. Race conditions are usually difficult to discover, because they occur intermittently.

To avoid this race condition, each thread should lock the data before accessing the counter and updating memory X. For example, if thread A takes a lock and updates the counter, it leaves memory X with a value of 2. After thread A releases the lock, thread B takes the lock and updates the counter, taking 2 as its initial value for X and increment it to 3, the expected result.