Conditional variable is a mechanism of synchronization between threads. This paper analyzes the implementation and use of conditional variable. Let's first look at the definition of conditional variables.
typedef struct { int c_spinlock; /* Spin lock to protect the queue. */ struct _pthread_queue c_waiting; /* Threads waiting on this condition. */ } pthread_cond_t;
We can see that the definition of conditional variables is very simple. Conditional variables are usually used together with mutually exclusive variables. The general process is as follows
Lock if ((conditions not met) { Blocking in condition variable } Operation locked resources Unlock
In fact, the mechanism is also very simple. The condition variable is to insert the thread into the waiting queue when the conditions are not met, and then wake up the thread in the queue when the conditions are met. Let's take a look at the specific implementation.
// Wait for blocking conditions. Before entering this function, mutex has been obtained int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) { volatile pthread_t self = thread_self(); // Lock operation queue acquire(&cond->c_spinlock); // Wait queue for insert condition enqueue(&cond->c_waiting, self); // Release lock after operation release(&cond->c_spinlock); // Release mutually exclusive variables, otherwise others will not be able to operate resources, resulting in conditions that cannot be met pthread_mutex_unlock(mutex); // Wake up after the pending waiting conditions are met suspend_with_cancellation(self); // Reacquire the mutex after being awakened pthread_mutex_lock(mutex); /* This is a cancellation point */ // The waiting period has been cancelled if (self->p_canceled && self->p_cancelstate == PTHREAD_CANCEL_ENABLE) { /* Remove ourselves from the waiting queue if we're still on it */ acquire(&cond->c_spinlock); // The thread is ready to exit and is removed from the conditional blocking queue remove_from_queue(&cond->c_waiting, self); release(&cond->c_spinlock); pthread_exit(PTHREAD_CANCELED); } return 0; }
pthread_ cond_ The wait function is a function called by the thread when the condition cannot be met. After calling, the thread will be suspended and wait to be awakened (if you don't want to be blocked all the time, you can call pthread_cond_timedwait. Pthread_cond_timedwait supports timed blocking). Take a look at the logic of suspending threads.
static inline void suspend_with_cancellation(pthread_t self) { sigset_t mask; sigjmp_buf jmpbuf; // Obtain the current signal shielding code sigprocmask(SIG_SETMASK, NULL, &mask); /* Get current signal mask */ // Clear pthread_ SIG_ The signal mask of restart, which allows the signal to be processed sigdelset(&mask, PTHREAD_SIG_RESTART); /* Unblock the restart signal */ /* No need to save the signal mask, we'll restore it ourselves */ /* The direct call returns 0, and the return from siglongjump returns non-0. Here, when the thread is suspended, Wake up after receiving the restart signal, or return here through signongjmp in the processing function of the cancellation signal */ if (sigsetjmp(jmpbuf, 0) == 0) { self->p_cancel_jmp = &jmpbuf; // If it has been cancelled and can be cancelled, it will be returned directly. Otherwise, it will be suspended waiting for wake-up if (! (self->p_canceled && self->p_cancelstate == PTHREAD_CANCEL_ENABLE)) { do { // Suspend waiting for restart signal sigsuspend(&mask); /* Wait for a signal */ } while (self->p_signal != PTHREAD_SIG_RESTART); } self->p_cancel_jmp = NULL; } else { // Return from the siglongjmp in the processing function of the cancel signal, reset the signal mask and mask the restart signal sigaddset(&mask, PTHREAD_SIG_RESTART); /* Reblock the restart signal */ sigprocmask(SIG_SETMASK, &mask, NULL); } }
We see that the thread is eventually suspended by calling sigsuspend. Wait for the signal to wake up. From the conditions of the while loop, we can see that when pthread is received_ SIG_ The thread will be "awakened" only when the restart signal is. Next, let's see how other threads wake up the blocked thread when the condition is met.
// If the conditions are met, wake up the thread int pthread_cond_signal(pthread_cond_t *cond) { pthread_t th; acquire(&cond->c_spinlock); // Take out a blocked thread th = dequeue(&cond->c_waiting); release(&cond->c_spinlock); // Send a signal to wake him up if (th != NULL) restart(th); return 0; } // Send wake-up signal to pid process static inline void restart(pthread_t th) { kill(th->p_pid, PTHREAD_SIG_RESTART); }
We see pthread_ cond_ The function of signal is very simple. Get a thread from the blocking queue and send him a wake-up signal. In addition, the thread library also supports waking up all threads.
// If the conditions are met, wake up all threads int pthread_cond_broadcast(pthread_cond_t *cond) { pthread_queue tosignal; pthread_t th; acquire(&cond->c_spinlock); /* Copy the current state of the waiting queue and empty it */ tosignal = cond->c_waiting; // Reset blocking queue queue_init(&cond->c_waiting); release(&cond->c_spinlock); /* Now signal each process in the queue */ // Send a signal to wake up all threads while ((th = dequeue(&tosignal)) != NULL) restart(th); return 0; }
pthread_cond_broadcast is to send a wake-up signal to each waiting thread. This is the principle and implementation of thread condition variables. Finally, let's look at an example.
struct prodcons { int buffer[BUFFER_SIZE]; /* Ring data buffer */ pthread_mutex_t lock; /* Mutex for accessing data area */ int readpos, writepos; /* Read / write pointer */ pthread_cond_t notempty; /* The conditional variable used by consumers. If it is not empty, there is data consumption */ pthread_cond_t notfull; /* The condition variable used by the producer. If it is not full, it can produce data */ }; struct prodcons buffer; void init(struct prodcons * b) { pthread_mutex_init(&b->lock, NULL); pthread_cond_init(&b->notempty, NULL); pthread_cond_init(&b->notfull, NULL); b->readpos = 0; b->writepos = 0; } int main() { pthread_t th_a, th_b; void * retval; // Initializes the data structure shared between threads init(&buffer); // Create two threads pthread_create(&th_a, NULL, producer, 0); pthread_create(&th_b, NULL, consumer, 0); pthread_join(th_a, &retval); pthread_join(th_b, &retval); return 0; }
Let's look at the logic of producers and consumers respectively
void * producer(void * data) { int n; for (n = 0; n < 10000; n++) { printf("%d --->\n", n); put(&buffer, n); } put(&buffer, OVER); return NULL; } void * consumer(void * data) { int d; while (1) { d = get(&buffer); if (d == OVER) break; printf("---> %d\n", d); } return NULL; }
We can see that the logic of producers and consumers is very simple, that is, one writes data to the buffer and the other reads data from the buffer. The question is, what if there is no space to write or no data to read? Let's look at the logic of the get and put functions.
void put(struct prodcons * b, int data) { // Operation of shared data requires locking pthread_mutex_lock(&b->lock); /* The write pointer + 1 is equal to the read pointer, indicating that there is no free space to write and waiting for free space */ while ((b->writepos + 1) % BUFFER_SIZE == b->readpos) { pthread_cond_wait(&b->notfull, &b->lock); } // pthread_ cond_ After being awakened in the wait, the mutex will be obtained again, so you can operate directly here b->buffer[b->writepos] = data; b->writepos++; // It's the tail. Correct the position if (b->writepos >= BUFFER_SIZE) b->writepos = 0; /* When there is data to consume, inform the waiting consumers */ pthread_cond_signal(&b->notempty); pthread_mutex_unlock(&b->lock); }
Then look at the implementation of get.
int get(struct prodcons * b) { int data; pthread_mutex_lock(&b->lock); /* If the read-write pointers are equal, there is no data to read. Wait for the data */ while (b->writepos == b->readpos) { pthread_cond_wait(&b->notempty, &b->lock); } data = b->buffer[b->readpos]; b->readpos++; if (b->readpos >= BUFFER_SIZE) b->readpos = 0; /* Consumption of data indicates that there is free space. Wake up the producer */ pthread_cond_signal(&b->notfull); pthread_mutex_unlock(&b->lock); return data; }
The above is the inter thread synchronization mechanism: the implementation and principle of conditional variables.