自学内容网 自学内容网

Linux线程同步机制之条件变量

线程同步的概念:

线程同步是多线程编程中的一个重要概念,它确保了多个线程在访问共享资源时能够协调一致,避免出现竞态条件、数据不一致或其他同步相关的问题。线程同步的关键在于控制多个线程的执行顺序和时机。

线程--条件变量的典型应用场景:

生产者-消费者问题

生产者线程生产数据,消费者线程消费数据。生产者在数据未消费完时可能需要等待,消费者在没有数据可消费时也需要等待。

a.组件:

仓库:用于存储产品,是生产者和消费者共享的资源。

生产者线程:负责生产产品并将其放入仓库。

消费者线程:从仓库中取出产品并进行消费

b.原理:

生产者线程

1.生产产品。

2.在仓库未满的情况下,将产品放入仓库。

3.如果仓库已满,生产者线程需要等待,直到消费者线程消费了某些产品,腾出空间。

消费者线程

1.消费产品。

2.在仓库不为空的情况下,从仓库取出产品进行消费。

3.如果仓库为空,消费者线程需要等待,直到生产者线程生产了新的产品

读者-写者问题

多个读者可以同时读取数据,但写者需要独占访问。

计数器问题

多个线程需要对同一个计数器进行递增操作,需要同步以保证计数的准确性

条件变量(Condition Variables)

条件变量是线程同步中的一种机制,它允许线程在某些条件不满足时挂起(即暂停执行),直到其他线程更改了条件并发出信号。条件变量通常与互斥锁结合使用,以确保线程安全

条件变量的主要特点:

  1. 等待条件变量的线程会释放互斥锁并挂起:当线程等待条件变量时,它必须首先拥有相关的互斥锁,然后释放该锁,进入等待状态。
  2. 条件变量被唤醒后需要重新获取互斥锁:当等待条件变量的线程被唤醒(通过信号),它将尝试重新获取之前释放的互斥锁。

条件变量原理:

step 1:消费者线程判断消费条件是否满足(仓库是否有产品),如果有产品则可以消费,然后解锁
step 2 :当条件满足时(仓库产品数量为0),则调用 pthread_cond_wait 函数,这个函数具体做的事
情如下:
在线程睡眠之前,对互斥锁解锁
让线程进入到睡眠状态
等待条件变量收到信号时,该函数重新竞争锁,并获取锁
step 3 :重新判断条件是否满足,如果满足,则继续调用 pthread_cond_wait 函数
step 4 :唤醒后,从 pthread_cond_wait 返回,条件不满足,则正常消费产品
step 5 :释放锁,整个过程结束

条件变量的作用:

1.减少CPU资源消耗,通过允许线程在条件不满足时挂起,而不是忙等待,从而减少CPU资源的消耗。

2.实现阻塞式同步,条件变量提供了一种机制,允许线程在未达到特定条件时阻塞,而不是不断地检查条件

3.实现复杂的同步逻辑,条件变量可以用于实现复杂的线程同步逻辑,如生产者-消费者问题

条件变量初始化:

条件变量的初始化是在使用条件变量之前必须进行的步骤

1.静态初始化

静态初始化是在编译时进行的,适用于在程序开始执行之前就已经确定了条件变量的使用场景

#include <pthread.h>

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

2.动态初始化

动态初始化是在程序执行过程中进行的,提供了更多的灵活性,比如可以设置条件变量的属性

函数头文件:
#include <pthread.h>

函数原型:
int pthread_cond_init(pthread_cond_t *restrict cond,
           const pthread_condattr_t *restrict attr);

函数参数:
cond:指向 pthread_cond_t 类型变量的指针,这个变量用于存储条件变量的状态。
attr:指向 pthread_condattr_t 类型变量的指针,定义了条件变量的属性。如果不需要特殊属性,可以传递 NULL

函数返回值:
成功:返回0,表示函数调用成功
失败: 返回错误码,以指示失败原因

错误码:
EAGAIN:系统资源不足(除了内存)
ENOMEM:初始化条件变量的内存不足
EINVAL:提供的属性无效

3.销毁条件变量

pthread_cond_destroy() 函数用于销毁先前初始化的条件变量,释放与条件变量关联的任何资源。当你的程序不再需要使用某个条件变量时,应该调用此函数进行清理

如果条件变量是通过 PTHREAD_COND_INITIALIZER 静态初始化的,则无需调用 pthread_cond_destroy(),因为静态初始化的条件变量在程序结束时会自动释放资源

函数头文件:
#include <pthread.h>

函数原型:
int pthread_cond_destroy(pthread_cond_t *cond);

函数参数:
cond:指向 pthread_cond_t 类型变量的指针,表示要销毁的条件变量。

函数返回值:
成功:返回0,表示函数调用成功
失败:返回错误码,以指示错误

错误码:
EBUSY:条件变量正在被使用,无法销毁。
EINVAL:指定的条件变量未被正确初始化,或者指向条件变量的指针无效。
EPERM:条件变量处于一个不一致的状态,例如,它可能与一个正在运行的线程状态相关联,而该线程异常终止了并且没有被join

 条件变量的信号通知函数:

pthread_cond_signal() 函数

pthread_cond_signal() 函数用于在 POSIX 线程编程中唤醒正在等待指定条件变量的至少一个线程。当条件变量被某个线程等待时,可以由另一个线程调用 pthread_cond_signal() 来唤醒等待的线程之一。

函数头文件:
#include <pthread.h>

函数原型:
int pthread_cond_signal(pthread_cond_t *cond);

函数参数:
cond:指向 pthread_cond_t 类型的变量的指针,该变量是之前已经初始化过的条件变量

函数返回值:
成功:返回0,表示函数调用成功
失败: 返回错误码,具体错误码会指出失败的原因

错误码:
EINVAL:提供的条件变量未正确初始化或指针无效

 注意事项:

pthread_cond_signal() 只会唤醒一个等待指定条件变量的线程。如果多个线程都在等待,将随机唤醒其中一个线程。

当你只需要通知一个线程,或者所有等待线程执行相同任务时,可以使用 pthread_cond_signal()

pthread_cond_broadcast() 函数

pthread_cond_broadcast() 函数用于唤醒所有等待指定条件变量的线程,这个函数是 POSIX 线程(pthread)库中条件变量操作的一部分,它允许一个线程通知多个其他线程关于某个条件的变化。

函数头文件:
#include <pthread.h>

函数原型:
int pthread_cond_broadcast(pthread_cond_t *cond);

函数参数:
cond:指向 pthread_cond_t 类型的变量的指针,该变量是之前已经初始化过的条件变量

函数返回值:
成功:返回0,表示函数调用成功
失败:返回错误码,具体错误码会指出失败的原因

错误码:
EINVAL:提供的条件变量未正确初始化或指针无效

 注意事项:

当有多个线程等待同一个资源,并且该资源可以被任意一个线程消费时,生产者线程可以调用 pthread_cond_broadcast() 来唤醒所有等待的消费者线程。

pthread_cond_broadcast() 会唤醒所有等待指定条件变量的线程。这在有多个线程等待同一个条件变量时非常有用。

pthread_cond_wait() 函数

pthread_cond_wait() 函数用于使当前线程挂起等待,直到其他线程对相同的条件变量发出信号(通过 pthread_cond_signal()pthread_cond_broadcast())。

函数头文件;
#include <pthread.h>

函数原型:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

函数参数:
cond:指向 pthread_cond_t 类型的变量的指针,该变量是之前已经初始化过的条件变量
mutex:指向 pthread_mutex_t 类型的变量的指针,该变量是与条件变量配合使用的互斥锁

函数返回值:
成功:返回0,表示函数调用成功
失败:返回错误码,具体的错误码指示失败原因

 条件变量与互斥锁的关联:

条件变量通常与互斥锁结合使用,以确保线程在检查条件和等待条件变量时数据的一致性和防止竞态条件

  1. 先获得锁:在对共享资源进行操作或检查条件之前,线程必须先获取关联的互斥锁。

  2. 检查条件:线程在获取互斥锁后会检查自己等待的条件是否满足。

  3. 调用 pthread_cond_wait()

    a.如果条件不满足,线程会调用 pthread_cond_wait() 函数挂起等待条件变量的信号。b.在等待期间,pthread_cond_wait() 函数会自动释放互斥锁,让其他线程有机会执行并改变条件。
  4. 阻塞线程:线程在 pthread_cond_wait() 调用中阻塞,直到另一个线程向相同的条件变量发出信号。

  5. 唤醒线程:当其他线程改变了条件并发出 pthread_cond_signal() 或 pthread_cond_broadcast() 信号后,等待的线程可能会被唤醒

  6. 重新竞争锁:被唤醒的线程会重新尝试获取之前释放的互斥锁。

  7. 重新获得锁:在成功获取互斥锁之后,线程再次检查条件是否满足。

  8. pthread_cond_wait() 函数返回

    a.如果线程被唤醒并且已经重新获得互斥锁,pthread_cond_wait() 函数会返回。b.线程此时会再次检查条件是否满足,如果满足则继续执行;如果不满足,则线程可能需要再次等待。
  9. 操作共享资源:如果条件满足,线程就可以对共享资源进行操作。

  10. 释放锁:操作完成后,线程释放互斥锁,以便其他线程可以获取锁并访问共享资源

 在生产者-消费者问题中使用条件变量:

生产者-消费者问题中,如果没有条件变量,生产者和消费者线程可能需要不断检查共享资源的状态,这会导致大量的CPU周期浪费在无效的检查上

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

#define MAX_GLOBAL 10000  // 定义生产者和消费者的最大产品数量

int global = 0;  // 全局计数器,用于生产的产品编号
int consumer_count = 0;  // 已消费的产品数量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  // 互斥锁,用于保护共享资源
pthread_cond_t cond_prod = PTHREAD_COND_INITIALIZER;  // 生产者条件变量
pthread_cond_t cond_cons = PTHREAD_COND_INITIALIZER;  // 消费者条件变量

void *producer(void *arg) {
    int count = atoi((char*)arg);
    for(int i = 0; i < count; i++) {
        pthread_mutex_lock(&mutex);
        while (global == MAX_GLOBAL) {  // 如果产品已满,等待消费者消费
            pthread_cond_wait(&cond_prod, &mutex);
        }
        printf("[%ld] Production product %d\n", pthread_self(), global++);
        pthread_cond_signal(&cond_cons);  // 通知消费者有新产品
        pthread_mutex_unlock(&mutex);
    }
    pthread_exit(NULL);
}

void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        while (global == 0) {  // 如果没有产品,等待生产者生产
            pthread_cond_wait(&cond_cons, &mutex);
        }
        printf("Consumer consumed product %d\n", global--);
        consumer_count++;
        if (consumer_count == MAX_GLOBAL) {  // 如果已消费的产品数量达到最大值,退出
            pthread_cond_signal(&cond_prod);  // 通知生产者停止生产
            pthread_mutex_unlock(&mutex);
            break;
        }
        pthread_cond_signal(&cond_prod);  // 通知生产者有空间生产新产品
        pthread_mutex_unlock(&mutex);
        sleep(1);  // 模拟消费时间
    }
    pthread_exit(NULL);
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Too few parameters!\n");
        exit(EXIT_FAILURE);
    }

    int product_number = 0;
    pthread_t tid_producer, tid_consumer;

    for (int i = 1; i < argc; i++) {
        product_number += atoi(argv[i]);
        if (pthread_create(&tid_producer, NULL, producer, (void*)argv[i])) {
            printf("producer %s\n", strerror(errno));
            exit(EXIT_FAILURE);
        }
    }

    if (pthread_create(&tid_consumer, NULL, consumer, NULL)) {
        printf("consumer %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    pthread_join(tid_producer, NULL);
    pthread_join(tid_consumer, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond_prod);
    pthread_cond_destroy(&cond_cons);

    return 0;
}

结语:

无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力


原文地址:https://blog.csdn.net/2301_79695216/article/details/142578073

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!