Linux线程同步

一、条件变量

当一个线程拿到锁,需要进入临界区读取数据时意外发现临界区中没有数据可读。如果不做处理,那么这个线程就会一直卡死,且锁无法归还导致BUG。

这时候,就需要将这个线程陷入睡眠状态,把锁归还交给其它线程进行处理,一旦有数据可读,该进程就会被唤醒进行操作。

这就是条件变量设计的目的。

pthread_cond_t类型

pthread_cond_t类型本质上也是一个结构体,我们不用关心里面有什么成员,只需要知道它是用来做线程等待 / 唤醒的对象。

我们可以抽象理解为:一个 "等待队列",里面记录了哪些线程正在睡觉、等条件。

pthread_cond_init 函数

复制代码
#include <pthread.h>

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

该函数的作用是对 pthread_cond_t 类型的条件变量(cond)进行初始化。

参数:

第一个参数 pthread_cond_t *cond 用于指定待初始化的条件变量对象,第二个参数 const pthread_condattr_t *attr 用于传入条件变量的属性配置,为 NULL 时表示使用默认属性。

pthread_cond_destroy函数

复制代码
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);

pthread_cond_destroy 用于销毁已初始化的 pthread_cond_t 条件变量,释放其占用的系统资源。

pthread_cond_wait函数

复制代码
#include <pthread.h>

int pthread_cond_wait(
    pthread_cond_t  *cond,    // 条件变量
    pthread_mutex_t *mutex    // 互斥锁
);

参数

cond:线程要在这个条件变量上休眠等待 ,唤醒也必须通过这个条件变量。

mutex:线程当前已经持有的互斥锁,函数会自动释放它,唤醒后又自动重新加锁。

具体作用:

pthread_cond_wait函数会使已经持有对应互斥锁的当前线程,以原子操作的方式先自动释放所持有的互斥锁 ,然后立即进入阻塞休眠状态 ,等待其他线程通过相同条件变量对其进行唤醒;当该线程被唤醒后,会自动再次尝试获取并持有该互斥锁,只有成功重新获取到互斥锁之后,该函数才会返回,线程才能继续执行后续代码。

pthread_cond_broadcast函数

复制代码
int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_broadcast 函数会唤醒所有正在通过同一个条件变量 cond 处于休眠等待状态的线程,使这些等待线程全部从 pthread_cond_wait 中开始尝试重新获取互斥锁,只有成功获取到锁的线程才能继续执行后续代码。

pthread_cond_signal函数

pthread_cond_signal 函数会唤醒指定条件变量等待队列中的一个线程 ,若存在多个等待线程,具体唤醒哪一个由操作系统调度策略决定,用户无法指定或预测 ;被选中的线程将从 pthread_cond_wait 的休眠状态退出,并尝试重新获取之前自动释放的互斥锁,只有成功获取到该互斥锁之后,线程才会从 pthread_cond_wait 函数返回并继续执行后续代码,其余未被唤醒的线程将继续保持休眠等待状态

复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 定义条件变量 + 互斥锁
pthread_cond_t cond;
pthread_mutex_t mutex;

// 等待线程:会先睡觉,等别人唤醒
void* wait_thread(void* arg)
{
    // 必须先加锁
    pthread_mutex_lock(&mutex);

    printf("等待线程:我要开始等待了...\n");

    // 自动解锁 + 休眠
    // 被唤醒后:自动重新加锁
    pthread_cond_wait(&cond, &mutex);

    printf("等待线程:我被唤醒啦!\n");

    // 解锁
    pthread_mutex_unlock(&mutex);
    return NULL;
}

// 唤醒线程:等一会儿再叫醒对方
void* signal_thread(void* arg)
{
    sleep(1);   // 让等待线程先运行

    pthread_mutex_lock(&mutex);
    printf("唤醒线程:我要唤醒等待线程...\n");

    // 唤醒 1 个在 cond 上等待的线程
    pthread_cond_signal(&cond);

    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main()
{
    pthread_t t1, t2;

    // 初始化
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);

    // 创建线程
    pthread_create(&t1, NULL, wait_thread, NULL);
    pthread_create(&t2, NULL, signal_thread, NULL);

    // 等待结束
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    // 销毁
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    return 0;
}

二、生产消费者模型

生产者 - 消费者模型就像商品生产商和超市顾客:生产商是生产者,负责把商品生产,顾客是消费者,从货架取走商品,货架满了生产商就只能等待,货架空了顾客就只能等待,一方完成操作腾出位置或补上货物后,就通知等待的一方继续,同时同一时间只允许一个人操作货架防止混乱,本质就是用等待和通知的规则让供货和购物高效有序、互不冲突地进行。

321原则

3 指三种核心关系:生产者与生产者互斥、消费者与消费者互斥、生产者与消费者既同步又互斥;

2 指两种核心角色:负责生产 / 放入数据的生产者线程、负责消费 / 取出数据的消费者线程;

1 指一个共享交易场所(如超市货架、内存缓冲区),作为生产者与消费者交互的唯一媒介。

为什么要使用生产者‑消费者模型

生产者‑消费者模型之所以存在,就是为了用一个中间容器(阻塞队列 / 缓冲区)把生产者和消费者彻底分开 ,生产者不直接跟消费者说话,消费者也不找生产者要数据,双方只依赖这个中间队列,这样生产者生产完数据直接丢进队列就可以继续生产,不用等待消费者处理,消费者也从队列里取数据,不用关心数据从哪来,中间队列起到了 "缓冲" 和 "桥梁" 的作用,从而把生产者和消费者的强耦合彻底解掉,同时平衡了它们的处理速度差异

生产者‑消费者模型的三个优点

1. 解耦

生产者只需要把数据放进中间队列,不用知道是谁来取、怎么处理,消费者也只从队列取数据,不用关心谁生产、生产了多少,两者不再直接依赖彼此,一方修改不会影响另一方,这就是 "解耦"。

2. 支持并发

生产者可以一边生产,消费者一边消费,互不等待,中间队列允许它们并发执行;同时通过互斥锁保护队列,队列又能缓冲突发数据,所以多线程可以同时工作、互不阻塞,整体并发效率更高。

3. 支持忙闲不均

生产者速度快、消费者慢时,队列可以缓存数据,不会让生产者停等;消费者快、生产者慢时,队列也能缓冲让消费者不急不忙,避免一方空闲一方忙的情况,使系统整体更平稳,这就是 "忙闲不均" 的平衡能力。

基于阻塞队列 BlockingQueue 的生产者消费者模型

复制代码
#define SIZE 10
#define HIGHT 8
#define LOW 3

namespace Block
{
    template <class T>
    class BlockQueue
    {
    public:
        BlockQueue(int hight = HIGHT, int low = LOW, int size = SIZE)
            : _cap(size), _hight(hight), _low(low)
        {
            pthread_mutex_init(&_mutex, nullptr);
            pthread_cond_init(&_consumer_cond, nullptr);
            pthread_cond_init(&_productor_cond, nullptr);
        }
        // 生产者生产商品
        void Enque(T &x)
        {
            pthread_mutex_lock(&_mutex);
            while (_cap == _que.size())
            {
                // 阻塞生产并等待,同时归还锁
                pthread_cond_wait(&_productor_cond, &_mutex);
            }
            _que.push(x);
            if (_que.size() >= _hight)
            {
                // 高水位喊消费者消费
                pthread_cond_signal(&_consumer_cond);
            }
            pthread_mutex_unlock(&_mutex);
        }

        // 顾客消费
        void Popque(T *x)
        {
            pthread_mutex_lock(&_mutex);
            while (_que.empty())
            {
                pthread_cond_wait(&_consumer_cond, &_mutex);
            }
            *x = _que.front(); // 拿出数据
            _que.pop();

            if (_que.size() <= _low)
            {
                // 低水位块没有东西可消费,喊生产商干活
                pthread_cond_signal(&_productor_cond);
            }
            pthread_mutex_unlock(&_mutex);
        }

        ~BlockQueue()
        {
            pthread_mutex_destroy(&_mutex);
            pthread_cond_destroy(&_consumer_cond);
            pthread_cond_destroy(&_productor_cond);
        }

    private:
        std::queue<T> _que;
        int _cap;
        pthread_mutex_t _mutex;

        // 给消费者和生产者的条件变量
        pthread_cond_t _consumer_cond;
        pthread_cond_t _productor_cond;

        // 高低水位
        int _hight;
        int _low;
    };

}

POSIX信号量

POSIX 信号量是用于实现线程或进程间同步与互斥的机制,本质就是一个计数器他的含义我们可以自定义(如可用空间、时事件发生次数等),它主要分为二值信号量 (0 或 1,相当于互斥锁)和计数信号量 (记录资源个数),核心操作只有两个:P 操作 (申请资源,计数器减 1,资源不足时阻塞等待)和 V 操作(释放资源,计数器加 1,唤醒等待的线程 / 进程)。

它既可以像互斥锁一样保证同一时间只有一个线程访问临界区实现互斥,也可以控制资源数量实现线程间的同步等待,比条件变量更简洁,常用来替代锁和条件变量完成生产者消费者模型。

sem_init函数

sem_initPOSIX 无名信号量的初始化函数,用于创建并初始化一个线程 / 进程间同步用的信号量

复制代码
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数 类型 含义 说明
sem sem_t * 信号量对象指针 指向已分配的 sem_t 变量(全局 / 栈 / 堆均可),不可为 NULL
pshared int 共享范围 0 :仅当前进程内线程共享;非 0:跨进程共享(需放共享内存)
value unsigned int 初始计数值 非负整数,代表可用资源数;0 表示初始阻塞,1 为二进制信号量(互斥锁)

成功:返回 0;失败:返回 -1,并设置 errno

sem_destroy函数

sem_destroy 用来销毁一个用 sem_init 初始化的无名信号量,释放其占用的内核资源。

复制代码
#include <semaphore.h>

int sem_destroy(sem_t *sem);

参数: sem:指向要销毁的信号量(必须是之前 sem_init 初始化过的)。

sem_wait函数

sem_waitPOSIX 信号量的核心操作(P 操作) ,作用是申请资源、阻塞等待 ,和 sem_init/sem_post/sem_destroy 配套使用。

复制代码
#include <semaphore.h>

int sem_wait(sem_t *sem);

功能:

信号量的值 -1,如果减完后 < 0,线程就阻塞等待。

若信号量 > 0:不阻塞,直接 -1,程序继续

若信号量 = 0:阻塞休眠,直到其他线程调用 sem_post 唤醒

sem_post函数

sem_post 是 POSIX 信号量的释放操作(V 操作) ,专门用来唤醒阻塞在 sem_wait 上的线程 / 进程 ,和 sem_wait天生一对

复制代码
#include <semaphore.h>

int sem_post(sem_t *sem);

功能:

信号量的值 +1,如果有加之前信号量 ≤ 0,就唤醒一个正在等待的线程。一次 sem_post 最多只唤醒一个线程,故存在信号值+1但没有唤醒线程的情况。

复制代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

// 定义一个全局信号量
sem_t sem;

// 子线程函数:等待信号量唤醒
void* thread_func(void* arg) {
    printf("子线程:等待 sem_wait...\n");
    sem_wait(&sem);  // P操作:申请资源,若sem=0则阻塞
    printf("子线程:被 sem_post 唤醒啦!\n");
    return NULL;
}

int main() {
    pthread_t tid;

    // 1. 初始化信号量:线程共享(pshared=0),初始值=0
    sem_init(&sem, 0, 0);

    // 2. 创建子线程
    pthread_create(&tid, NULL, thread_func, NULL);

    // 主线程休眠1秒,确保子线程先运行并阻塞在sem_wait
    sleep(1);
    printf("主线程:调用 sem_post 唤醒子线程\n");

    // 3. V操作:信号量+1,唤醒阻塞的子线程
    sem_post(&sem);

    // 等待子线程结束
    pthread_join(tid, NULL);

    // 4. 销毁信号量
    sem_destroy(&sem);

    printf("主线程:程序结束\n");
    return 0;
}
相关推荐
tumeng07112 小时前
Linux(CentOS)安装 Nginx
linux·nginx·centos
cyber_两只龙宝2 小时前
【Docker】Docker的原生网络介绍
linux·运维·docker·云原生·容器
AzusaFighting2 小时前
Dify (Ubuntu 24.04 Noble x64)部署教程
linux·运维·ubuntu
xlp666hub2 小时前
一篇文章彻底搞懂Linux驱动的并发控制与中断上下半部机制
linux·面试
木心月转码ing2 小时前
三个小技巧(commit message规范、代码格式化技巧、WSL开启网络代理)
linux
wang09073 小时前
Linux性能优化之上下文切换
linux·运维·性能优化
bellus-3 小时前
ubuntu24安装
linux
守护安静星空3 小时前
ubuntu vscode 调试 at32f435vmt7基于AT32IDE
linux·运维·笔记·vscode·ubuntu
誰能久伴不乏3 小时前
从数字世界到物理引擎:用 PWM 撕开 0 和 1 的结界
linux·arm开发·c++·qt