Article count:1385 Read by:1972833

Featured Content
Account Entry

Linux multithreaded synchronization mechanism--conditional variables

Latest update time:2024-07-09
    Reads:

I. Introduction

Condition variables, as one of the core synchronization mechanisms in multithreaded programming, are designed to solve the problem of threads pausing execution while waiting for a condition to be met. It allows threads to suspend gracefully when the condition is not met, releasing CPU resources until the condition is modified to a satisfied state by other threads, so that they are awakened and continue to execute. The introduction of condition variables not only optimizes the performance of the program, but also greatly simplifies the complexity of synchronization and communication between threads. It is one of the key tools for building efficient and stable multithreaded applications.

2. Basic Concepts

Condition variables are an important tool for multithreaded programming to achieve inter-thread communication and synchronization. In essence, it is a "flag" for a thread to wait for. When this "flag" is set to a specific state, the waiting thread will be awakened and continue to execute. Specifically, condition variables allow one or more threads to suspend execution and wait for a specific condition to occur. This condition is usually related to the state of shared resources or the results of operations of other threads. When the condition is not met, the thread will be blocked on the condition variable, releasing CPU the resource for other threads to use. Once the condition is met, other threads can notify the thread waiting on the condition variable through specific operations to resume execution.

3. Working Principle

Condition variables are often used in conjunction with mutexes to ensure safe access to shared resources and conditions. When a thread wants to wait for a condition to be met, it first needs to acquire the mutex associated with it to ensure that it will not be interfered with by other threads when checking and operating the condition. The thread then checks whether the condition has been met. If the condition is not met, the thread calls the waiting function of the condition variable (such as pthread_cond_wait ), and in the process automatically releases the previously acquired mutex and enters the waiting state. When another thread completes the operation on the shared resource and the condition is met, it acquires the same mutex and then wakes up the thread waiting on the condition variable by calling the notification function of the condition variable (such as pthread_cond_signal or pthread_cond_broadcast ). The awakened thread does not start executing immediately, but will re-compete for the previously released mutex. Only the thread that successfully acquires the mutex will check again whether the condition is indeed met. If it is met, it will continue to perform subsequent operations; if the condition is still not met, the thread will enter the waiting state again. This mechanism ensures safe coordination between threads on shared resources and condition states, avoiding race conditions and inconsistencies.

[ Question 1 ] If the condition is not met, the thread uses pthread_cond_wait() to suspend, and in the process automatically releases the mutex lock acquired before, and enters the waiting state. Is the mutex lock released here by pthread_cond_wait() the function?

[ Answer ] Yes, when a thread calls pthread_cond_wait() or pthread_cond_timedwait() functions, they will release the mutex internally first, and then suspend the thread to wait for the condition variable. Once another thread calls pthread_cond_signal() or pthread_cond_broadcast() to wake up the waiting thread, or pthread_cond_timedwait() a timeout occurs in the case of , the waiting thread will try to acquire the mutex again and then recheck the previously suspended condition.

The following are pthread_cond_wait() typical usage patterns for :

  1. The thread first locks the mutex.
  2. Check whether the condition is met.
  3. If the condition is not met, the thread calls pthread_cond_wait() and passes in the address of the mutex.
  4. pthread_cond_wait() The mutex is released inside the function and the thread is put into a waiting state.
  5. When the condition variable is triggered, the waiting thread is awakened.
  6. The thread tries again to lock the mutex (this may involve waiting if some other thread currently holds the lock).
  7. Once the mutex is reacquired, the thread checks the condition again.

pthread_cond_wait() Example code using :

pthread_mutex_lock(&mutex);
while (!condition) {
pthread_cond_wait(&cond, &mutex); // 释放互斥锁并等待条件变量
}
// 条件已满足,继续执行
pthread_mutex_unlock(&mutex);

In this pattern, condition the condition that the thread is waiting for is the mutex associated mutex with the condition variable . When the condition is not met, it is called and the thread releases the mutex and waits. When the condition is met and signaled by another thread, the waiting thread wakes up, reacquires the mutex, and checks the condition again. cond pthread_cond_wait()

This mechanism ensures that CPU resources are not wasted constantly checking conditions during the waiting period, and also avoids race conditions caused by multiple threads accessing shared resources simultaneously when the conditions are met.

Question 2 】When the condition is met and a signal is sent by other threads, the waiting thread will be awakened, reacquire the mutex, and check the condition again. Who will reacquire the mutex here? Is it pthread_cond_wait() implemented inside the function? And who will check the condition again?

[ Answer ] Yes, when the condition is met and a signal is sent by other threads, the process of waking up the waiting thread is pthread_cond_wait() implemented inside the function. The following are the detailed steps:

  1. Wake up the thread : When pthread_cond_signal or pthread_cond_broadcast is called, the thread waiting for the condition variable will be woken up.

  2. Reacquire the mutex : The awakened thread pthread_cond_wait() automatically attempts to reacquire the mutex that was previously released before returning. This is pthread_cond_wait() the part of the function that ensures that the mutex is locked before the thread returns from the wait state and continues execution.

  3. Recheck the condition : After the thread reacquires the mutex, it needs to recheck whether the condition it was previously suspended on is still valid. This is because other threads may have changed the condition while the thread was waiting, or multiple threads may have been awakened but the condition is only valid for one of them.

    Here is pthread_cond_wait() a typical code example using , showing how to wait on a condition variable and how to recheck the condition after being woken up:

    pthread_mutex_lock(&mutex);
    while (!condition) {
    // 等待条件变量,互斥锁在此过程中会被释放
    pthread_cond_wait(&cond, &mutex);
    // 这里互斥锁已经被pthread_cond_wait内部重新获取
    }
    // 条件已满足,继续执行
    // 此时互斥锁是锁定的,共享资源是安全的
    pthread_mutex_unlock(&mutex);

In this example, condition the condition that the thread is waiting for is the mutex associated mutex with the condition variable . When the condition is not met, the thread calls and releases the mutex, then suspends waiting. When the condition is met and a signal is sent by another thread, the waiting thread is awakened, the function helps the thread reacquire the mutex, and then the thread checks the condition again. cond pthread_cond_wait() pthread_cond_wait()

This mechanism ensures that threads have mutually exclusive access to shared resources during the waiting and waking process, thus avoiding race conditions. Developers need to take this into account when designing synchronization logic to ensure that the use of condition variables is safe.

4. Function prototype

A condition variable is a synchronization mechanism used to wait for a condition to occur in multithreaded programming. In C the language, condition variables are API usually provided by POSIX the thread library ( pthreads ). The following are the main functions related to condition variables API , including their function prototypes, parameters, and return values:

  1. pthread_cond_init() - Initialize condition variables

  • cond : pthread_cond_t Pointer to a structure used to create a condition variable.
  • attr : pthread_condattr_t Pointer to a structure containing the properties of the condition variable. If None NULL , the default properties are used.
  • Function prototype:

    int pthread_cond_init(pthread_cond_t *restrict cond,
                          const pthread_condattr_t *restrict attr)
    ;
  • parameter:

  • Return value: Returns 0 if successful, and returns the corresponding error code if an error occurs.

  • pthread_cond_destroy() - Destroy the condition variable

    • cond : Pointer to a previously initialized condition variable.
    • Function prototype:

      int pthread_cond_destroy(pthread_cond_t *cond);
    • parameter:

    • Return value: Returns 0 if successful, and returns the corresponding error code if an error occurs.

  • pthread_cond_wait() - Waiting on a condition variable

    • cond : Pointer to the condition variable.
    • mutex : Pointer to a locked mutex. This mutex must be locked before waiting on the condition variable and will be released during the wait.
    • Function prototype:

      int pthread_cond_wait(pthread_cond_t *restrict cond,
                            pthread_mutex_t *restrict mutex)
      ;
    • parameter:

    • Return value: Returns 0 on success, and returns the corresponding error code when an error occurs or the function is awakened.

  • pthread_cond_timedwait() - Wait condition variable with timeout

    • cond : Pointer to the condition variable.
    • mutex : Pointer to the locked mutex.
    • abstime : struct timespec A pointer to , indicating the timeout period. This is an absolute time, usually clock_gettime() set using the function to get the current time and add the timeout period.
    • Function prototype:

      int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                                 pthread_mutex_t *restrict mutex,
                                 const struct timespec *restrict abstime)
      ;
    • parameter:

    • Return value: 0 if successful and not timed out, an error code if an error occurs, or a timeout if the error occurs ETIMEDOUT .

  • pthread_cond_signal() - wake up a thread waiting on a condition variable

    • cond : Pointer to the condition variable.
    • Function prototype:

      int pthread_cond_signal(pthread_cond_t *cond);
    • parameter:

    • Return value: Returns 0 if successful, and returns the corresponding error code if an error occurs.

  • pthread_cond_broadcast() - Wake up all threads waiting on the condition variable

    • cond : Pointer to the condition variable.
    • Function prototype:

      int pthread_cond_broadcast(pthread_cond_t *cond);
    • parameter:

    • Return value: Returns 0 if successful, and returns the corresponding error code if an error occurs.

  • pthread_condattr_init() - Initialize condition variable attributes

    • attr : pthread_condattr_t Pointer to structure.
    • Function prototype:

      int pthread_condattr_init(pthread_condattr_t *attr);
    • parameter:

    • Return value: Returns 0 if successful, and returns the corresponding error code if an error occurs.

  • pthread_condattr_destroy() - Destroys condition variable attributes

    • attr : Pointer to a previously initialized condition variable attribute.
    • Function prototype:

      int pthread_condattr_destroy(pthread_condattr_t *attr);
    • parameter:

    • Return value: Returns 0 if successful, and returns the corresponding error code if an error occurs.

    These functions complete POSIX the conditional variables in the thread library API , allowing developers to implement complex synchronization logic in multithreaded programs. Correct use of these functions API is essential to avoiding race conditions, deadlocks, and other synchronization problems.

    V. Characteristics and Challenges

    Condition variables have the following advantages:

    1. Efficient collaboration: Through conditional variables, threads can wait when the condition is not met, avoiding invalid busy loops and improving CPU resource utilization.
    2. Flexible communication: Allows threads to wait and wake up based on complex conditions, enhancing the flexibility and accuracy of communication between threads.
    3. Reduce contention: It can effectively coordinate threads' access to shared resources, reducing contention and conflict between threads.

    However, there are some challenges with using condition variables:

    1. Programming complexity: Using condition variables requires careful handling of the interaction between mutex locks and condition variables. Improper use may lead to deadlocks, race conditions, and other difficult-to-debug errors.
    2. Risk of spurious wakeup: Although uncommon, there is a possibility that a thread may be woken up spurious, that is, the thread is woken up when the condition is not met. Therefore, when using condition variables, it is usually necessary to check the condition again in the loop waiting for the condition.
    3. Difficult to understand: For beginners, it may be difficult to understand the working principle and correct use of conditional variables, which requires a deep understanding of the concept of thread synchronization.

    6. C language implementation example

    Here is a producer-consumer model implementation using condition variables:

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

    // 定义缓冲区大小
    #define BUFFER_SIZE 10

    // 定义缓冲区结构,包含数据缓冲、索引、互斥锁和条件变量
    typedef struct {
        int buffer[BUFFER_SIZE]; // 数据缓冲区
        int in, out;             // 索引,in指向下一个写入位置,out指向下一个读取位置
        pthread_mutex_t mutex;   // 互斥锁,用于同步对缓冲区的访问
        pthread_cond_t notFull; // 条件变量,生产者在缓冲区未满时等待
        pthread_cond_t notEmpty; // 条件变量,消费者在缓冲区非空时等待
    } Buffer;

    // 初始化缓冲区
    void buffer_init(Buffer* buf) {
        buf->in = buf->out = 0// 初始化索引
        pthread_mutex_init(&buf->mutex, NULL); // 初始化互斥锁
        pthread_cond_init(&buf->notFull, NULL); // 初始化notFull条件变量
        pthread_cond_init(&buf->notEmpty, NULL); // 初始化notEmpty条件变量
    }

    // 生产者线程函数
    voidproducer(void* arg) {
        Buffer* buf = (Buffer*)arg; // 从传入的参数中获取Buffer结构的指针。
        int value = 1// 初始化生产的数据值。

        while (value <= BUFFER_SIZE) { // 当生产的数据值小于或等于BUFFER_SIZE时循环。
            pthread_mutex_lock(&buf->mutex); // 锁定互斥锁,进入临界区。

            // 检查缓冲区是否已满。如果满了,生产者将等待。
            while ((buf->in + 1) % BUFFER_SIZE == buf->out) {
                pthread_cond_wait(&buf->notFull, &buf->mutex);
                // 如果缓冲区满,生产者在notFull条件变量上等待,同时保持互斥锁。
            }

            // 缓冲区未满,生产者可以放入数据。
            buf->buffer[buf->in] = value; // 将数据放入缓冲区。
            buf->in = (buf->in + 1) % BUFFER_SIZE; // 更新生产索引,如果达到末尾则回到开始位置。

            printf("Produced value: %d\n", value); // 打印生产的数据值。

            // 通知消费者,缓冲区中有新数据可以消费。
            pthread_cond_signal(&buf->notEmpty);

            pthread_mutex_unlock(&buf->mutex); // 释放互斥锁,退出临界区。

            value++; // 准备生产下一项数据。
            usleep(500000); // 线程休眠一段时间,模拟生产过程所需时间。
        }
        return NULL// 线程结束。
    }

    // 消费者线程函数
    voidconsumer(void* arg) {
        Buffer* buf = (Buffer*)arg; // 从传入的参数中获取Buffer结构的指针。
        int value; // 用于存储从缓冲区取出的数据。

        while (1) { // 无限循环,直到消费者决定退出。
            pthread_mutex_lock(&buf->mutex); // 锁定互斥锁,进入临界区。

            // 检查缓冲区是否为空。如果为空,消费者将等待。
            while (buf->in == buf->out) {
                pthread_cond_wait(&buf->notEmpty, &buf->mutex);
                // 如果缓冲区空,消费者在notEmpty条件变量上等待,同时保持互斥锁。
            }

            // 缓冲区不为空,消费者可以取出数据。
            value = buf->buffer[buf->out]; // 从缓冲区取出数据。
            buf->out = (buf->out + 1) % BUFFER_SIZE; // 更新消费索引,如果达到末尾则回到开始位置。

            printf("Consumed value: %d\n", value); // 打印消费的数据值。

            // 通知生产者,缓冲区有空间可以生产更多数据。
            pthread_cond_signal(&buf->notFull);

            pthread_mutex_unlock(&buf->mutex); // 释放互斥锁,退出临界区。

            if (value >= BUFFER_SIZE) break// 如果取出的数据值达到或超过BUFFER_SIZE,退出循环。
            usleep(500000); // 线程休眠一段时间,模拟消费过程所需时间。
        }
        return NULL// 线程结束。
    }

    int main() {
        pthread_t prod, cons; // 线程ID
        Buffer buf; // 创建缓冲区实例

        // 初始化缓冲区
        buffer_init(&buf);

        // 创建生产者线程
        if (pthread_create(&prod, NULL, producer, &buf) != 0) {
            perror("Failed to create producer thread");
            exit(EXIT_FAILURE);
        }

        // 创建消费者线程
        if (pthread_create(&cons, NULL, consumer, &buf) != 0) {
            perror("Failed to create consumer thread");
            exit(EXIT_FAILURE);
        }

        // 等待生产者线程结束
        pthread_join(prod, NULL);
        // 等待消费者线程结束
        pthread_join(cons, NULL);

        // 清理互斥锁和条件变量
        pthread_mutex_destroy(&buf.mutex);
        pthread_cond_destroy(&buf.notFull);
        pthread_cond_destroy(&buf.notEmpty);

        printf("Production and consumption complete.\n");

        return 0;
    }

    The key points in this example are detailed below:

    1. Buffer size definition ( BUFFER_SIZE ) :

    • BUFFER_SIZE Is a macro that defines the size of the ring buffer. This value determines how many data items the buffer can store. In the producer-consumer model, the size of the buffer directly affects the synchronization behavior of the producer and consumer threads.
  • Buffer structure ( Buffer ) :

    • Buffer The structure contains all the elements needed for the buffer: an integer array for storing data ( buffer[] ), two integer variables in and out used as indexes, pointing to the next production and consumption positions respectively. In addition, it contains a mutex ( mutex ) for synchronizing access to the buffer, and two condition variables ( notFull and notEmpty ) for synchronizing the behavior of producers and consumers respectively.
  • Producer function ( producer ) :

    • producer The function simulates the behavior of a producer. It generates a series of data and tries to put this data into a buffer. If the buffer is full, the producer will wait on notFull the condition variable until space is available in the buffer. The producer uses a mutex to ensure that the buffer is not accessed by other threads while putting data.
  • Consumer function ( consumer ) :

    • consumer The function simulates the behavior of the consumer. It takes data from the buffer and processes it. If the buffer is empty, the consumer will wait on notEmpty the condition variable until there is data to be taken from the buffer. The consumer also uses a mutex to ensure the safety of the buffer when taking data.
  • Time simulation ( usleep ) :

    • usleep The function is used to make the thread sleep for a specified period of time (in microseconds). In this example, usleep the time delay required for production and consumption operations is simulated, which helps to observe and understand the synchronization behavior between threads.
  • Initialization and thread creation in the main function :

    • In main the function, first initialize Buffer the structure, including the mutex and condition variable. Then create the producer and consumer threads to execute the producer and consumer functions respectively.
  • Wait for thread to complete( pthread_join ) :

    • Use to pthread_join wait for the producer and consumer threads to complete their tasks. This function call blocks until the specified threads finish. This is key to ensure that the program does not exit before all threads have completed.
  • Clean up resources :

    • After all threads have completed, use pthread_mutex_destroy and pthread_cond_destroy to clean up the mutex and condition variables and release the resources they occupy.
  • Synchronization mechanism demonstration :

    • This model shows how to use condition variables and mutexes to synchronize access to a shared resource (buffer). Producers and consumers decide whether to continue or wait based on the state of the buffer (full or empty) and are awakened when the condition is met.

    Through this example, we can see the powerful role of condition variables in multi-threaded synchronization. They provide an effective way to coordinate cooperation between threads and ensure correct and safe access to shared resources.

    Compile and execute the program. The results are as follows:

    [root@localhost cond]# gcc pthread_cond_test.c -o pthread_cond_test -lpthread
    [root@localhost cond]# ls
    pthread_cond_test pthread_cond_test.c
    [root@localhost cond]# ./pthread_cond_test
    Produced value: 1
    Consumed value: 1
    Produced value: 2
    Consumed value: 2
    Produced value: 3
    Consumed value: 3
    Produced value: 4
    Consumed value: 4
    Produced value: 5
    Consumed value: 5
    Produced value: 6
    Consumed value: 6
    Produced value: 7
    Consumed value: 7
    Produced value: 8
    Consumed value: 8
    Produced value: 9
    Consumed value: 9
    Produced value: 10
    Consumed value: 10
    Production and consumption complete.

    This output verifies the correct implementation of the producer-consumer model, where condition variables and mutexes are used to ensure that data items can be safely passed between producers and consumers.

    VII. Conclusion

    Condition variables are an important tool for implementing complex synchronization logic in multi-threaded programming, but their correct use is not easy. Developers need to have a deep understanding of their working principles and mechanisms, and carefully handle various details and potential problems. Only in this way can we give full play to the advantages of condition variables and build efficient, stable and reliable multi-threaded applications. At the same time, continuous practice and experience accumulation are also the key to mastering condition variables. Through application and debugging in actual projects, developers can use this powerful synchronization mechanism more skillfully and improve their multi-threaded programming capabilities and levels.


     
    EEWorld WeChat Subscription

     
    EEWorld WeChat Service Number

     
    AutoDevelopers

    About Us Customer Service Contact Information Datasheet Sitemap LatestNews

    Room 1530, Zhongguancun MOOC Times Building,Block B, 18 Zhongguancun Street, Haidian District,Beijing, China Tel:(010)82350740 Postcode:100190

    Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved 京ICP证060456号 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号