Analysis of Inter-thread Communication 1: mutex and condition Variables

Source: Internet
Author: User

Analysis of Inter-thread Communication 1: mutex and condition Variables

The purpose of thread synchronization is to ensure data consistency. In Linux, common thread synchronization methods include mutex, read/write locks, and conditional variables. Reasonable Use of these three methods can ensure data consistency. However, it is worth noting that, when designing an application, all threads must comply with the same data access rules to ensure that these Synchronization Methods are valid. If a thread is allowed to have no access permission (such as a lock) when accessing the shared resources, other threads get the lock before using the shared resources, and data inconsistency also occurs. There are also spin locks, barrier and semaphore thread synchronization methods. This article will discuss the use of mutex and conditional variables, and provide the relevant code and precautions. The relevant code is also downloaded on my github.

 

Mutex

 

In essence, mutex is a lock. You can actively lock the mutex before accessing shared resources. After the access is complete, you must release the lock on the mutex. After the mutex is locked, other threads will be blocked when trying to lock the mutex until the lock is released (Note that if the thread tries to lock the same mutex twice, then it will be blocked, that is, a deadlock .). If multiple threads are blocked when the mutex lock is released, all the threads that are blocked in the mutex will become runable. The first thread that becomes runable will obtain the mutex lock, other threads will be blocked again. (Note that the lock acquisition of the blocking thread is related to the implementation and is uncertain. For example, it may be that the thread with the highest priority will be awakened first, but the thread with the same priority will be given, then follow the FIFO principle to wake up .) The following code uses mutex to solve the typical producer-consumer problem:

 

#include 
 
  #include 
  
   #include 
   
    #define MAXNITEMS       1000000#define MAXNTHREADS         100#define MIN(a,b) (((a) < (b))?(a):(b))int     nitems;         /* read-only by producer and consumer,to express maximum item */struct {  pthread_mutex_t   mutex;  int   buff[MAXNITEMS];  int   nput;  int   nval;} shared = { PTHREAD_MUTEX_INITIALIZER };void    *produce(void *), *consume(void *);int main(int argc, char **argv){    int         i, nthreads, count[MAXNTHREADS];    pthread_t   tid_produce[MAXNTHREADS], tid_consume;    if (argc != 3)    {        printf(usage: prodcons4 <#items> <#threads>);        return 1;    }    nitems = MIN(atoi(argv[1]), MAXNITEMS);    nthreads = MIN(atoi(argv[2]), MAXNTHREADS);    printf(main:%d,%d,%d,shared.nput,shared.nval,shared.buff[0]);    /* create all producers and one consumer */    for (i = 0; i < nthreads; i++) {        count[i] = 0;        pthread_create(&tid_produce[i], NULL, produce, &count[i]);    }       pthread_create(&tid_consume, NULL, consume, NULL);    /* wait for all producers and the consumer */    for (i = 0; i < nthreads; i++) {        pthread_join(tid_produce[i], NULL);        printf(count[%d] = %d, i, count[i]);        }    pthread_join(tid_consume, NULL);    exit(0);}void * produce(void *arg){    for ( ; ; ) {        pthread_mutex_lock(&shared.mutex);        if (shared.nput >= nitems) {            pthread_mutex_unlock(&shared.mutex);            return(NULL);       /* array is full, we're done */        }        shared.buff[shared.nput] = shared.nval;        shared.nput++;        shared.nval++;        pthread_mutex_unlock(&shared.mutex);        *((int *) arg) += 1;    }}void consume_wait(int i){    for ( ; ; ) {        pthread_mutex_lock(&shared.mutex);        if (i < shared.nput) {            pthread_mutex_unlock(&shared.mutex);            return;         /* an item is ready */        }        pthread_mutex_unlock(&shared.mutex);        sched_yield();    }}void * consume(void *arg){    int     i;    for (i = 0; i < nitems; i++) {        consume_wait(i);        if (shared.buff[i] != i)            printf(buff[%d] = %d, i, shared.buff[i]);    }    return(NULL);}
   
  
 

 

The result is as follows:

 

$gcc -Wall -lpthread mutex_example.c -o mutex_example$./mutex_example 1000000 5count[0] = 188090count[1] = 197868count[2] = 194924count[3] = 211562count[4] = 207556

 

The above program implements synchronization between multiple producer threads and one consumer thread, which is worth noting in the following aspects:

I) The struct shared contains a mutex variable and the data to be synchronized. The purpose of encapsulating them into a struct is to emphasize that these variables should only be accessed when they have their mutex lock. It is a good programming technique to encapsulate shared data and their synchronization variables (mutex locks, conditional variables, or semaphores) into a struct.

II) In function produce (), the addition of the count element of each thread does not belong to the lock critical section, because each thread is counted by its respective counters [I], always minimize the amount of code locked by a mutex.

III) In the consume () function, the consume_wait (I) function is called to check whether the expected entries are ready. If not, sched_yield () is called, the consumption thread will give up the current CPU and move the thread to the end of the corresponding priority queue. Here, the consumption thread gives the CPU to the producer thread. But this is still not an ideal method. The ideal method is to wake up the consumer thread when the buff contains data (or when there is a specified number of data.

 

Condition variable

Conditional variables are another synchronization mechanism available for threads. The mutex is used for locking, the condition variable is used for waiting, and the condition variable always needs to be used together with the mutex. The following code re-implements the above consumer-producer problem with condition variables:

 

#include 
 
  #include 
  
   #include 
   
    #define MAXNITEMS       1000000#define MAXNTHREADS         100#define MIN(a,b) (((a) < (b))?(a):(b))int     nitems;             /* read-only by producer and consumer,to express maximum item */int     buff[MAXNITEMS];struct {  pthread_mutex_t   mutex;  int               nput;   /* next index to store */  int               nval;   /* next value to store */} put = { PTHREAD_MUTEX_INITIALIZER };struct {  pthread_mutex_t   mutex;  pthread_cond_t    cond;  int               nready; /* number ready for consumer */} nready = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER };int     nsignals; /*call pthread_cond_signal count*/void    *produce(void *), *consume(void *); int main(int argc, char **argv){    int         i, nthreads, count[MAXNTHREADS];    pthread_t   tid_produce[MAXNTHREADS], tid_consume;    if (argc != 3)    {           printf(usage: prodcons4 <#items> <#threads>);        return 1;    }       nitems = MIN(atoi(argv[1]), MAXNITEMS);    nthreads = MIN(atoi(argv[2]), MAXNTHREADS);    /* create all producers and one consumer */for (i = 0; i < nthreads; i++) {        count[i] = 0;        pthread_create(&tid_produce[i], NULL, produce, &count[i]);    }    pthread_create(&tid_consume, NULL, consume, NULL);    /* wait for all producers and the consumer */    for (i = 0; i < nthreads; i++) {        pthread_join(tid_produce[i], NULL);        printf(count[%d] = %d, i, count[i]);    }    pthread_join(tid_consume, NULL);    printf(nsignals = %d, nsignals);    exit(0);}void * produce(void *arg){    for ( ; ; ) {        pthread_mutex_lock(&put.mutex);        if (put.nput >= nitems) {            pthread_mutex_unlock(&put.mutex);            return(NULL);       /* array is full, we're done */        }        buff[put.nput] = put.nval;        put.nput++;        put.nval++;        pthread_mutex_unlock(&put.mutex);        pthread_mutex_lock(&nready.mutex);        if (nready.nready == 0) {            pthread_cond_signal(&nready.cond);            nsignals++;        }        nready.nready++;        pthread_mutex_unlock(&nready.mutex);        *((int *) arg) += 1;    }}void * consume(void *arg){    int     i;    for (i = 0; i < nitems; i++) {        pthread_mutex_lock(&nready.mutex);        while (nready.nready == 0)            pthread_cond_wait(&nready.cond, &nready.mutex);        nready.nready--;        pthread_mutex_unlock(&nready.mutex);        if (buff[i] != i)            printf(buff[%d] = %d, i, buff[i]);    }    return(NULL);}
   
  
 

 

The result is as follows:

 

$gcc -Wall -lpthread cond_var_example.c  -o cond_var_example$./cond_var_example  1000000 5count[0] = 220234count[1] = 201652count[2] = 199165count[3] = 182972count[4] = 195977nsignals = 3

 

The above program implements synchronization between multiple producer threads and one consumer thread, which is worth noting in the following aspects:

I) due to the uncertainty of scheduling, the program running results are different each time.

II) In consume (), we can see that the consumption thread waits for nready through a while loop. nready (used to count the number of items currently for processing by the consumer) is changed to non-0, if nready. if the nready value is 0, call pthread_cond_wait () to block the thread and bring it to sleep until other threads call pthread_cond_signal () to wake it up, this thread remains in sleep state and does not participate in scheduling or consume CPU. Note the following when calling the pthread_cond_wait () function:

A. Before the thread calls the pthread_cond_wait () function, it must obtain the mutex associated with it before calling the change interface. In the preceding example, it is the counter nready. the mutex of nready. mutex, the thread can determine nready only after obtaining the mutex. whether nready is zero. If yes, call the pthread_cond_wait () interface with the mutex address as the parameter. Call pthread_cond_wait if no mutex is obtained. The result is undefined.

B. Call the pthread_cond_wait () function to perform the following two actions atomically: first, assign the mutex to nready. mutex unlock; then put the calling thread into sleep until another thread calls pthread_cond_signal for this condition variable. Note that these two operations must be atomic operations. Otherwise, if the thread calls pthread_cond_signal before it is unlocked and put into sleep, the current thread will not be notified after it is put into sleep, that is, the condition change is missed. Here it is nready. nready value change. After the above two operations are performed, the thread is sleep. At this time, the pthread_cond_wait () function has not returned.

C. When other threads call pthread_cond_signal or pthread_cond_broadcast, the thread waiting for the corresponding conditional variable will be awakened. At this time, the Awakened thread can participate in scheduling, at this time, the Awakened thread continues to execute the pthread_cond_wait () function. Before the pthread_cond_wait () function returns the result, it will lock the mutex corresponding to the condition variable again. Here it is nready. mutex. If the function returns successfully, the current thread obtains nready again. mutex lock, of course nready. mutex may also be occupied by other threads, and the thread is blocked again. In short, before the function pthread_cond_wait () returns a successful result, it is bound to lock the corresponding mutex. From the call to the return of pthread_cond_wait, the entire process is equivalent to: unlock, just_wait, lock, and the first two operations are atomic operations.

D. After pthread_cond_wait () is returned successfully, you need to re-check whether the corresponding condition is true. Here is the value of nready. nready. Possible false wake-up: the expected condition (here the value of nready. nready is not 0 in the consumption thread) cannot be immediately awakened. Various thread implementations should minimize the number of false wakeups.

III) In produce (), we can see that in nready. before the nready value is added to 1, if the counter value is 0, call pthread_cond_signal to wake up the thread that may be waiting for its value to become non-zero. We can see that the mutex is used to synchronize access to nready. nready, and the associated condition variable is used to wait and send signals. If only mutex is used for synchronization, the previous busy wait may occur (even if sched_yield is called, it will only delay). After the conditional variable is used, the thread can block sleep, until the required conditions occur (here nready. nready value is non-zero, and the thread that changes this condition also notifies the thread that is blocked in the corresponding condition variable by calling pthread_cond_signal or pthread_cond_broadcast using the condition variable as the parameter.

IV) before calling the pthread_cond_signal function, you do not have to lock the mutex variable (of course, there is no release of mutex when the returned value is returned). However, you usually need to lock the variable when modifying the condition variable, this leads to a problem. Pthread_cond_signal can be placed between pthread_mutex_lock and pthread_mutex_unlock, or after pthread_mutex_lock and pthread_mutex_unlock. In the above example, the first method is used. That is, the following form:

 

pthread_mutex_lockxxxxxxxpthread_cond_signalpthread_mutex_unlock
The disadvantage of doing so is that, when the conditional variable is sent a signal, the system immediately schedules it to wait for the thread on it, and the thread starts to run, however, before pthread_cond_wait returns, you need to obtain the mutex again. At this time, the mutex is occupied by other threads (that is, the thread that wakes it up), so the wake-up thread is blocked again, in this way, there will be performance problems. But this problem does not occur in LinuxThreads or NPTL, because in Linux threads, there are two queues: cond_wait queue and mutex_lock queue, pthread_cond_signal only allows the thread to move from the cond_wait queue to the mutex_lock queue, without returning it to the user space. There is no performance loss. Another example is as follows:

 

 

pthread_mutex_lockxxxxxxxpthread_mutex_unlockpthread_cond_signal

 

The code for the above example can be changed:

 

int dosignal;pthread_mutex_lock(&nready.mutex);dosignal = (nready.nready == 0);pthread_mutex_unlock(&nready.mutex);if (dosignal)pthread_cond_signal(&nready.cond);
The advantage of this form is that there will be no potential performance loss mentioned previously, because the lock has been released before the notification wait thread; the disadvantage is that if the unlock and pthread_cond_signal are, if a low-priority thread is waiting on mutex, the low-priority thread will seize the high-priority thread (call the pthread_cond_wait thread ), however, this will not happen in the middle-end mode above.

 

V) generally, the pthread_cond_signal function only wakes up a thread waiting on the corresponding conditional variable. If no thread is in the blocking wait state variable, pthread_cond_signal will return success. If multiple threads are blocking and waiting for this condition variable, determine which thread receives the signal based on the priority of each waiting thread to continue execution. If the priority of each thread is the same, the thread obtains the signal based on the length of wait time. However, no matter how one pthread_cond_signal call can send a message at most once (according to POSIX, at least one thread wakes up, and this interface implementation may also wake up multiple threads ). In some cases, you may need to wake up multiple threads (for example, a writer can wake up multiple reader threads). In this case, you can call pthread_cond_broadcast to wake up all threads that are blocked on the corresponding conditional variables.

VI) In general, the code for sending signals to conditional variables is roughly as follows:

 

Struct {pthread_mutex_t mutex; pthread_cond_t cond; maintain various variables of this condition} var = {PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER}; pthread_mutex_lock (& var. mutex); set the condition to pthread_cond_signal (& var. cond); pthread_mutex_unlock (& var. mutex );
In our example, the variable used to maintain the condition is an integer counter. The operation to set the condition is to add 1 to the counter. We have optimized the processing, that is, the conditional variable signal is sent only when the counter changes from 0 to 1. The code for testing the condition and Entering sleep to wait for the condition variable to be true is roughly as follows:

 

 

Pthread_mutex_lock (& var. mutex); while (the condition is false) pthread_cond_wait (& var. cond, & var. mutex); Modify the condition pthread_mutex_unlock (& var. mutex );
VI) Why do I need to lock a mutex when using conditional variables? The implementation of conditional variables is based on the obtained mutex of locks. Call pthread_cond_wait if no mutex is obtained. The result is undefined. This mutex is used to access the protection conditions themselves.

 

 

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.