[Linux——Lesson24.线程:线程同步与互斥]

目录

前言

本节重点:

一、😎Linux线程互斥

二、🤔线程锁

2-1 🍕互斥量Mutex

2-2 🍔解除与销毁锁

三 、😗问题解决及线程饥饿

四、😀互斥锁的底层实现

4-1 🍟互斥量的封装

五、😋线程同步

六、🤩条件变量

6-1 🍿条件变量函数

6-2 🍳条件变量示例

🗝️总结与提炼

结束语


前言

在 Linux 多线程编程中,线程间的并发执行虽能充分利用 CPU 资源、提升程序效率,但也伴随线程安全的核心挑战 —— 共享资源竞争易导致数据不一致,而线程执行的无序性可能引发逻辑混乱。为解决这些问题,线程互斥与同步成为关键技术支撑。

本文将从 Linux 线程互斥的核心需求切入,逐步拆解线程锁(尤其是互斥量)的使用与管理逻辑,剖析线程饥饿等潜在问题的解决方案及互斥锁底层实现原理;再延伸至线程同步范畴,重点详解条件变量的函数接口与实战应用。通过 “问题 - 方案 - 原理 - 实例” 的层层递进,构建完整的 Linux 线程同步互斥知识体系,为多线程编程的安全性与高效性提供理论与实操指引。

本节重点:

  • 深刻理解线程互斥的原理和操作
  • 深刻理解线程同步
  • 掌握⽣产消费模型
  • 设计⽇志和线程池
  • 理解线程安全和可重⼊,掌握锁相关概念

一、😎Linux线程互斥

  • 临界资源多线程执行流共享的资源就叫做临界资源
  • 临界区每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

如果不能保持互斥,那么会发生一些不合逻辑的事情,以下面这段多线程抢票代码为例:

#include <iostream>
#include <vector>
#include "thread.hpp" // 自己实现的线程封装

using namespace ThreadModule;

// 数据不一致
int g_tickets = 10000; // 共享资源,没有保护的, 多线程同时访问

void route(int &tickets)
{
    while (true)
    {
        if(tickets>0) // 票数小于0, 终止抢票
        {
            usleep(1000);
            printf("get tickets: %d\n", tickets);
            tickets--;
        }
        else
        {
            break;
        }
    }
}

const int num = 4;
int main()
{
    // std::cout << "main: &tickets: " << &g_tickets << std::endl;

    std::vector<Thread<int>> threads;
    // 1. 创建一批线程
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        threads.emplace_back(route, g_tickets, name);
    }

    // 2. 启动 一批线程
    for (auto &thread : threads)
    {
        thread.Start();
    }

    // 3. 等待一批线程
    for (auto &thread : threads)
    {
        thread.Join();
        std::cout << "wait thread done, thread is: " << thread.name() << std::endl;
    }

    return 0;
}

这里线程对同一个共享资源进行操作,进行并发执行类似 “抢票” 的模式,但是最后得到的数据却发现,抢票居然还有负数?这种情况我们称为 数据不一致

 这个问题是怎么产生怎么导致的呢?首先我们先要了解一个概念:原子性,前面我们说,原子性只有两态,要么已完成,要么未完成。实际上,在编程的角度来说,原子性指的是汇编层面只有一条语句比如对一个内置类型进行赋值操作,在汇编层面其实就是一条move指令。所以其是原子的。

了解了上述概念之后,我们再来看一看代码的逻辑结构,在route函数里,我们对tickets进行了判断,而判断是逻辑运算,需要在CPU内进行操作。

判断完成后,刚刚进入内部,执行usleep()函数,所以此时线程就被切换,进入到等待队列。假设此时是thread-1在跑,又因为tickets被保存到寄存器当中,而thread-1此时要进行线程切换则需要带走thread-1的数据,则此时thread-1把寄存器中的tickets带走了。(线程等待结束后才会继续执行后续代码)
随后thread-2也开始执行此函数,因为上一个thread-1线程遇到了usleep,所以后续的tickets- -,以及total- - 都是没有执行的。也就是说上一次对tickets操作后tickets值并没有变,所以此时thread-2同样将内存中的tickets加载到寄存器当中,同样,tickets此时的值还是1,同样thread-2遇到usleep,那么thread-2也要带着自己的数据到等待队列当中。

把全局变量加载到CPU不是本质,本质是 将共享的全局变量加载到寄存器使得当前线程私有化共享全局变量。而此时寄存器的值又没有被写回,所以此时thread-2也进入到等待队列。同理,周而复始,thread-3 4都是如此。

而当等待队列等待完成后,所有线程都开始执行后续的代码,之前阻塞到printf,这里printf不影响tickets所以忽略。后续代码执行到 tickets-- ⇒ tickets = tickets - 1; 此步操作不是原子的,因为它需要经历:1、从内存读取到CPU. 2、CPU内部进行- -操作. 3、写回到内存中, 那么此时问题就出来了。
 当thread-1已经将tickets进行了–,并且将其写回到了内存。那么接下来thread-2等待完成又对tickets进行–,此时CPU中的tickets已经变为0了,所以–过后tickets变为了-1,再次将其写回到内存当中。周而复始,使得原本正常的抢票,最后却变为了负数。
 

也就是说,共享资源(tickets)被访问时没有被保护起来,并且本身操作不是原子的。

二、🤔线程锁

上面我们已经把问题给搞明白了,接下来我们需要解决问题,如何解决这种线程问题呢?通常的解决方案是对线程进行加锁。

2-1 🍕互斥量Mutex

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来一些问题。为了解决上述问题:
 

  • 代码必须要有互斥行为当代码进入临界区执行时,不允许其他线程进入该临界区
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

为此,Linux给我们提供了互斥锁,首先我们先来认识一下这些接口:

🚩初始化互斥量的两种方式

如果定义的锁是静态或者全局的

 使用 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 宏进行初始化互斥量,那么这把锁就可以直接使用了。

如果定义的是局部的锁(动态的,比如临时对象)

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

函数参数

  • mutex要初始化的互斥量
  • attrNULL

申请锁方式

 不论我们使用哪种方式定义上面的锁,我们都可以对这把锁进行上锁 pthread_lock():

申请锁接口

int pthread_mutex_lock(pthread_mutex_t *mutex);

 使用该接口只有三种结果:

  1. 申请成功,函数会返回,允许继续向后执行
  2. 申请失败,函数会阻塞,不允许向后运行
  3. 函数调用失败,出错返回
调⽤ pthread_ lock 时,可能会遇到以下情况:
  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调⽤会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁。

尝试申请锁

int pthread_mutex_trylock(pthread_mutex_t *mutex);

尝试申请锁,与pthread_mutex_lock()唯一不同的是,当申请锁失败之后,不会进行阻塞等待,而是直接出错返回,并设置错误码返回出错原因。

2-2 🍔解除与销毁锁

解除互斥锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

 有加锁必然有解锁,当线程在临界资源内执行完毕后,需要释放当前锁,让其他线程进入,所以需要释放锁。

销毁互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);
销毁互斥量需要注意:
  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

三 、😗问题解决及线程饥饿

出现数据不一致问题的本质是,多个执行流并发访问全局数据的代码所导致的访问公共资源的代码,我们称为临界区

 我们加锁的本质是把并行的执行流改变为串行的执行流,而对临街资源的保护实质上就是对临街区代码的加解锁。

#include <iostream>
#include <vector>
#include "thread.hpp"

using namespace ThreadModule;

// 数据不一致
int g_tickets = 10000; // 共享资源,没有保护的, 多线程同时访问

class ThreadData
{
public:
    ThreadData(int tickets, const std::string &name):_tickets(tickets), _name(name), _total(0)
    {}

    ~ThreadData()
    {}
public:
    int &_tickets; // 所有的线程最后都会引用同一个全局的g_tickets
    std::string _name;
    int _total;
};

pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;// 创建互斥量

void route(ThreadData *td)
{
    while (true)
    {
        // 加锁力度越细越好
        pthread_mutex_lock(&gmutex);// 上锁
        if(td->_tickets>0)
        {
            usleep(1000);
            printf("%s running, get tickets: %d\n",td->_name.c_str(),  td->_tickets);
            td->_tickets--;
            pthread_mutex_unlock(&gmutex);// 解锁
            td->_total++;// 将解锁放在此句后面也是可以的,只不过这里的total已经不属于临界区了,所以如果要严格按照规则加锁解锁,就在上一句进行解锁
        }
        else
        {
            break;
        }
    }
}

const int num = 4;// 创建线程数
int main()
{
    // std::cout << "main: &tickets: " << &g_tickets << std::endl;

    std::vector<Thread<ThreadData*>> threads;
    std::vector<ThreadData*> datas;
    // 1. 创建一批线程
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        ThreadData* td = new ThreadData(g_tickets, name);
        threads.emplace_back(route, td, name);
        datas.emplace_back(td);
    }

    // 2. 启动 一批线程
    for (auto &thread : threads)
    {
        thread.Start();
    }

    // 3. 等待一批线程
    for (auto &thread : threads)
    {
        thread.Join();
        // std::cout << "wait thread done, thread is: " << thread.name() << std::endl;
    }

    // 4. 输出统计数据
    for(auto & data:datas)
    {
        std::cout << data->_name << " : " << data->_total << std::endl;
        delete data;
    }

    return 0;
}

这样加锁了之后,就不会再出现之前的情形,数据也就正常了。但是如果你是CentOS的用户的话,是有一些bug的,因为CentOS环境中,某些线程的竞争能力太强了,以至于得到的结果往往只有一个线程有结果,其他线程为0,这是因为在CentOS中对线程调度的算法没有Unbuntu的新,也就是没有Ubuntu的算法好。
 所以,又能得出另一个结论:多线程加锁,这些多线程对锁的竞争是自由的如果竞争能力太强的线程,会导致其他线程抢不到锁,也就造成了线程饥饿问题!我所说的CentOS的这种行为就是竞争饥饿问题。

四、😀互斥锁的底层实现

  • 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。现在我们把lock和unlock的伪代码改一下


  swap或者exchange可以交换寄存器和内存单元中的值,第一句movb 0, al,把左值赋值给寄存器al,第二步xchgb把寄存器内的值和Mutex锁进行交换,随后判断 寄存器内的内容是否>0,如果是则返回0,表示加锁成功,否则就挂起等待, 表示当前锁被线程等待,等待完成继续执行锁。
解锁的过程,此时线程已经执行完毕,把寄存器中的值重新放进内存的mutex变量中,表示当前锁已经释放。下图或许能帮助你更好的理解这一过程:

 为什么线程能做这件事呢?我们之前说过,CPU寄存器内部的数据,保存了线程的硬件上下文,而数据在内存里,所有线程都能够访问,属于共享的,但是如果转移到CPU内部的寄存器中,就属于一个线程私有了

 上图中,线程1因为某些原因需要线程切换,进入等待队列。那么此时线程1需要把自己的上下文数据带走,其实就是把寄存器当中保存的值带走,并且没有对内存交换的0进行写回,也就是说此时内存中的mutex是0,那么线程2在交换mutex到寄存器当中,就会进行状态检测,此时检测到状态为0,说明当前已经有人占用锁了,则线程2进入到挂起状态,后来的线程依旧会如此,直到第一个线程执行完毕将锁释放。
 所以上述所谓的交换就显得尤为重要,这里的交换指的不是单纯的拷贝,而是所有线程在争锁的时候只有一个值,而这个值往往就是那把锁。交换过程只有一条汇编语句,所以 交换过程是原子的那么就能保证交换时不会发生线程切换这样的事情。

临界区内部正在访问临界区的线程,此时能否被调度切换呢?

一个线程在访问临界区时,对于其他线程来说,

1、锁被释放。

2、曾经没有申请到锁正在挂起状态。此时当前线程访问临街资源是加了锁的,对其他线程来说这一过程是原子的,所以说此时访问临界区资源是线程安全的。

4-1 🍟互斥量的封装

Lock.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdlib>  // for exit
#include <string.h> // for strerror

namespace LockModule
{
// 互斥锁封装类
class Mutex
{
public:
    // 禁用拷贝和赋值(互斥锁不可复制)
    Mutex(const Mutex&) = delete;
    Mutex& operator=(const Mutex&) = delete;

    // 构造函数:初始化互斥锁(默认属性)
    Mutex()
    {
        int ret = pthread_mutex_init(&_mutex, nullptr);
        if (ret != 0)
        {
            std::cerr << "Mutex初始化失败: " << strerror(ret) << std::endl;
            std::exit(EXIT_FAILURE); // 初始化失败直接退出,避免后续错误
        }
    }

    // 带属性的构造函数(支持自定义锁属性,如递归锁)
    explicit Mutex(pthread_mutexattr_t* attr)
    {
        int ret = pthread_mutex_init(&_mutex, attr);
        if (ret != 0)
        {
            std::cerr << "Mutex带属性初始化失败: " << strerror(ret) << std::endl;
            std::exit(EXIT_FAILURE);
        }
    }

    // 加锁
    void Lock()
    {
        int ret = pthread_mutex_lock(&_mutex);
        if (ret != 0)
        {
            std::cerr << "Mutex加锁失败: " << strerror(ret) << std::endl;
            std::exit(EXIT_FAILURE);
        }
    }

    // 解锁
    void Unlock()
    {
        int ret = pthread_mutex_unlock(&_mutex);
        if (ret != 0)
        {
            std::cerr << "Mutex解锁失败: " << strerror(ret) << std::endl;
            std::exit(EXIT_FAILURE);
        }
    }

    // 获取原始互斥锁指针(用于条件变量等场景)
    pthread_mutex_t* GetMutexOriginal()
    {
        return &_mutex;
    }

    // 析构函数:销毁互斥锁
    ~Mutex()
    {
        int ret = pthread_mutex_destroy(&_mutex);
        if (ret != 0)
        {
            std::cerr << "Mutex销毁失败: " << strerror(ret) << std::endl;
            // 析构函数中不建议exit,仅打印错误
        }
    }

private:
    pthread_mutex_t _mutex; // 底层互斥锁对象
};

// RAII风格的锁管理类(自动加锁/解锁)
class LockGuard
{
public:
    // 禁用拷贝和赋值(避免锁管理混乱)
    LockGuard(const LockGuard&) = delete;
    LockGuard& operator=(const LockGuard&) = delete;

    // 构造时加锁
    explicit LockGuard(Mutex& mutex) : _mutex(mutex)
    {
        _mutex.Lock();
    }

    // 析构时解锁
    ~LockGuard()
    {
        _mutex.Unlock();
    }

private:
    Mutex& _mutex; // 引用外部的互斥锁
};
} // namespace LockModule

代码功能解析

Mutex 类

封装了 pthread_mutex_t 类型的互斥锁,提供了初始化、加锁、解锁、销毁等操作的接口,同时禁用了拷贝构造和赋值运算符(互斥锁是不可复制的资源)。

  • 构造函数:通过 pthread_mutex_init 初始化互斥锁,使用默认属性(nullptr)。
  • Lock()/Unlock():分别调用 pthread_mutex_lock 和 pthread_mutex_unlock 实现加锁和解锁。
  • GetMutexOriginal():返回原始互斥锁指针,方便与其他 POSIX 线程接口(如条件变量 pthread_cond_t)配合使用。
  • 析构函数:通过 pthread_mutex_destroy 销毁互斥锁,释放资源。

LockGuard 类

采用 RAII(资源获取即初始化)设计模式,通过构造函数获取锁,析构函数自动释放锁,确保锁的正确管理(即使代码中出现异常或提前返回,也不会导致锁泄漏)。

优点

  1. 简化锁的使用:封装了底层 pthread 函数,避免直接操作原始互斥锁,降低使用成本。
  2. 防止死锁风险LockGuard 自动管理锁的生命周期,无需手动调用 Unlock(),减少人为失误导致的死锁。
  3. 禁止复制:通过 = delete 禁用 Mutex 的拷贝和赋值,符合互斥锁的唯一性语义(复制锁会导致资源管理混乱)。

抢票程序(ticket_seller.cpp)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>  // 用于错误信息
#include "Lock.hpp"

using namespace LockModule;

int ticket = 1000;
Mutex mutex;  // 全局互斥锁,保护ticket

// 线程执行函数:抢票逻辑
void* route(void* arg) {
    const char* id = static_cast<const char*>(arg);  // 规范类型转换
    while (1) {
        int current_ticket = 0;  // 保存当前要卖出的票号
        {
            // 缩小锁的作用域:仅保护共享变量的读写
            LockGuard lockguard(mutex);
            if (ticket <= 0) {
                break;  // 票已售罄,退出循环
            }
            current_ticket = ticket;  // 先记录当前票号
            ticket--;  // 减少票数
        }  // 锁在此处自动释放,其他线程可抢票

        // 耗时操作放在锁外,提高并发效率
        usleep(1000);  // 模拟出票耗时
        printf("%s sells ticket: %d\n", id, current_ticket);
    }
    printf("%s exit\n", id);  // 线程退出提示
    return nullptr;
}

int main(void) {
    pthread_t t1, t2, t3, t4;
    int ret;

    // 创建线程1
    ret = pthread_create(&t1, NULL, route, (void*)"thread 1");
    if (ret != 0) {
        fprintf(stderr, "pthread_create t1 failed: %s\n", strerror(ret));
        return 1;
    }

    // 创建线程2
    ret = pthread_create(&t2, NULL, route, (void*)"thread 2");
    if (ret != 0) {
        fprintf(stderr, "pthread_create t2 failed: %s\n", strerror(ret));
        return 1;
    }

    // 创建线程3
    ret = pthread_create(&t3, NULL, route, (void*)"thread 3");
    if (ret != 0) {
        fprintf(stderr, "pthread_create t3 failed: %s\n", strerror(ret));
        return 1;
    }

    // 创建线程4
    ret = pthread_create(&t4, NULL, route, (void*)"thread 4");
    if (ret != 0) {
        fprintf(stderr, "pthread_create t4 failed: %s\n", strerror(ret));
        return 1;
    }

    // 等待线程结束
    ret = pthread_join(t1, NULL);
    if (ret != 0) {
        fprintf(stderr, "pthread_join t1 failed: %s\n", strerror(ret));
    }

    ret = pthread_join(t2, NULL);
    if (ret != 0) {
        fprintf(stderr, "pthread_join t2 failed: %s\n", strerror(ret));
    }

    ret = pthread_join(t3, NULL);
    if (ret != 0) {
        fprintf(stderr, "pthread_join t3 failed: %s\n", strerror(ret));
    }

    ret = pthread_join(t4, NULL);
    if (ret != 0) {
        fprintf(stderr, "pthread_join t4 failed: %s\n", strerror(ret));
    }

    printf("All threads exit, ticket sold out\n");  // 所有线程结束提示
    return 0;
}

五、😋线程同步

 主线开始前,我们先来听一个故事:

 20年前,阿飞在xx大学上学,当时信号交通不便,他们学校西门只有一个电话庭,阿飞每次打电话都会去这个电话亭,一直让阿飞感到难受的是,电话庭太少了,人却太多了,每个人都想打电话。这一天阿飞早早的来到了电话庭,恰巧这时候没人,他是第一个,于是给异地的女朋友打了两个小时电话,这个时候阿飞看时间不早了,想要去吃中午饭,吃完饭继续再跟女朋友聊
 

 但是呢这个时候阿飞回头一看,阿飞刚出电话亭,就看到密密麻麻站满了人,“这吃完饭再来不得到猴年马月才能打上电话?” 于是阿飞咬咬牙,大不了中午不吃饭了,说完,因为他距离门最近,他又进去把门关了,又叙了两个小时。随后阿飞痛快的出门,可是刚出门就想起来自己没生活费了,然后又急忙转身进入电话亭把门关上,又给家里打了电话。这样来来回回好几次,一直占着电话亭。
  此时电话亭外面的人不乐意了,“怎么还xx的不出来,再不出来劳资见你一次打你一次!”,阿飞眼看着局势不对,也不敢出电话亭,于是就拨通了警察局的电话,警察来了之后,了解了大致情况。于是在电话亭这里设立了警戒线,并且装上了高清摄像头,并规定:每个人来到这里以后必须要排队,并且打完电话的人不能再次直接进入,必须从队尾重新排队打电话。

 

其实上面这个故事就是今天的主线,线程同步,为什么这么说呢?我们把人比作线程,在警察来之前,线程一直在占用这个锁,导致其他线程没办法拿到锁,一直处于等待状态,就会产生线程饥饿问题。而第二种情况,每个线程在没拿到锁之前都需要排队等待,并且拿到锁的线程如果要二次进入则需要重新到队尾排队。
 而上述的过程基本上做到了让不同线程在保证电话亭安全的前提下,让所有的线程访问临界资源具有了一定的顺序性。这个工作我们称为 线程同步

  • 同步在保证 数据安全 的前提下,让线程能够按照某种特定的顺序访问 临界资源,从而有效避免 饥饿问题,叫做 同步

六、🤩条件变量

实现线程同步,我们常用做法是使用条件变量。这里的条件变量可不是环境变量,那什么是条件变量呢?

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了

例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

条件变量是多线程编程中用于实现线程间同步的一种机制,它通常与互斥锁(mutex)配合使用,解决线程间因共享资源状态变化而需要等待或唤醒的问题。

核心作用

当线程持有互斥锁访问共享资源时,若资源状态不满足操作条件(例如队列空、缓冲区满),线程可以通过条件变量暂时释放互斥锁并进入等待状态;直到其他线程修改资源状态并通过条件变量唤醒它,该线程才会重新获取互斥锁并继续执行。

关键操作

  1. 等待(wait)线程调用 wait(mutex) 时,会原子性地释放持有的互斥锁,并阻塞等待条件变量被唤醒。被唤醒后,线程会重新竞争获取互斥锁,成功后继续执行。

  2. 唤醒(signal/broadcast)

    • signal()唤醒至少一个等待该条件变量的线程。
    • broadcast()唤醒所有等待该条件变量的线程。

示例场景(生产者 - 消费者模型)

假设有一个共享队列,生产者线程向队列添加元素,消费者线程从队列取出元素:

  • 消费者发现队列为空时,通过条件变量等待,释放锁让生产者能操作队列。
  • 生产者添加元素后,通过条件变量唤醒等待的消费者。
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;

// 消费者线程
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx); // 获取锁
        // 等待队列非空(防止虚假唤醒,用while循环判断)
        cv.wait(lock, []{ return !q.empty(); });
        
        int val = q.front();
        q.pop();
        lock.unlock(); // 可选:提前释放锁,减少阻塞时间
        // 处理元素...
    }
}

// 生产者线程
void producer() {
    while (true) {
        int val = /* 生成数据 */;
        std::lock_guard<std::mutex> lock(mtx); // 获取锁
        q.push(val);
        cv.notify_one(); // 唤醒一个等待的消费者
    }
}

int main() {
    std::thread c(consumer);
    std::thread p(producer);
    c.join();
    p.join();
    return 0;
}

注意事项

  1. 必须与互斥锁配合:条件变量本身不提供互斥,需通过互斥锁保护共享资源的访问。
  2. 防止虚假唤醒wait 需配合循环判断条件(如示例中的 while (!q.empty())),因为线程可能在未被显式唤醒的情况下恢复执行(操作系统调度原因)。
  3. 唤醒时机signal 或 broadcast 应在修改共享资源状态并释放锁之前调用(或在持有锁时调用,确保状态已更新)。

条件变量高效解决了线程间 “等待 - 唤醒” 的同步问题,避免了线程忙轮询(反复检查条件)带来的资源浪费。

6-1 🍿条件变量函数

条件变量初始化(动态,局部条件变量):

#include <pthread.h>

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

函数参数

  • cond要初始化的条件变量
  • attrNULL

静态,全局条件变量初始化

pthread_cond_t cond cond = PTHREAD_COND_INTIALIZER;

  这里与互斥锁规则相似,不再过多赘述。

销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足

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

函数参数

  • cond要在这个条件变量上等待(队列内等待)
  • mutex互斥量
  • 细节当线程条件变量wait的时候,会释放此时持有的锁,当被唤醒时,需要重新竞争锁,只有竞争成功才会唤醒,否则一直处于竞争状态

唤醒线程:

int pthread_cond_broadcast(pthread_cond_t *cond);// 唤醒所有在cond等待下的线程
int pthread_cond_signal(pthread_cond_t *cond);// 唤醒一个线程

 以上接口的返回值,全部都是:返回0为成功,失败设置错误码。要学习条件变量实际上上面这些接口就足够了。

6-2 🍳条件变量示例

这里使用全局条件变量,全部使用接口调用的形式展示条件变量的作用:

      创建一个主控线程,3个附属线程,对三个附属线程进行cond等待,通过主控线程唤醒这些线程(全部唤醒和单独唤醒)。

 main函数内定义一个接收tid的数组,一函数调用的形式分别创建一个主控线程和多个附属线程:

int main()
{
    std::vector<pthread_t> tids;

    StartMaster(&tids);// 主控线程
    StartSlaver(&tids);// 其他线程

    WaitThread(tids);// 线程等待
    return 0;
}

 添加需要的头文件,以及设置全局条件变量与全局互斥锁:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>

pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;// 全局条件变量
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;// 全局互斥量

 创建附属线程,默认创建3个附属线程,所有附属线程执行同一回调SlaverCore,回调内将所有线程在临界区内加锁并等待,此时线程锁gmutex释放 线程进入cond等待队列,等待主控线程唤醒,下一个线程重复此步操作,直至所有现成进入到cond等待队列,等待主控唤醒:

void* SlaverCore(void* args)
{
    std::string name = static_cast<const char*>(args);
    while(true)
    {
        // 1. 加锁
        pthread_mutex_lock(&gmutex);
        // 2. 一般条件变量是在加锁和解锁之间使用
        pthread_cond_wait(&gcond, &gmutex);// gmutex: 这个是用来释放的[前一半],进入等待队列,此时锁被释放
        std::cout << "当前被叫醒的线程是:" << name << std::endl;
        pthread_mutex_unlock(&gmutex);
    }
    return nullptr;
}

void StartSlaver(std::vector<pthread_t> *tidsptr, int threadnum = 3)
{
    for(int i = 0; i < threadnum; ++i)
    {
        char *name = new char[64];// 每一个线程都需要new 一个新名字,否则很可能会出现线程覆盖问题
        snprintf(name, 64, "slaver-%d", i + 1);
        pthread_t tid;
        int n = pthread_create(&tid, nullptr, SlaverCore, name);
        if(n == 0)
        {
            std::cout << "create sucess: " << name << std::endl;
            tidsptr->emplace_back(tid);
        }
    }
}

 主控线程,创建主控线程,执行主控回调,主控回调函数内,休眠三秒确保所有附属线程进入等待队列,在循环里可选择的将所有线程选择全部唤醒或者隔一秒唤醒一个线程:

void* MasterCore(void *args)// call back func
{
    sleep(3);
    std::cout << "master start work..." << std::endl;
    std::string name = static_cast<const char*>(args);
    while(true)
    {
        // pthread_cond_signal(&gcond);// 一次唤醒1个线程
        pthread_cond_broadcast(&gcond);// 广播唤醒所有的线程
        std::cout << "master awake a thread..." << std::endl;
        sleep(1);
    }
    return nullptr;
}

void StartMaster(std::vector<pthread_t> *tidsptr)// main contrl thread
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, MasterCore, (void*)"Master Thread");
    if(n == 0)
    {
        std::cout << "create master success" << std::endl;
    }
    tidsptr->emplace_back(tid);
}

 main-thread阻塞等待回收所有线程:

void WaitThread(std::vector<pthread_t> &tids)
{
    for(auto & tid : tids)
    {
        pthread_join(tid, nullptr);
    }
}

主控线程一次性全部唤醒等待队列的线程:

主控线程每隔一秒唤醒一个线程:

🗝️总结与提炼

本文围绕 Linux 多线程环境下的协作与冲突问题,系统梳理了线程互斥与同步的核心技术及实现逻辑,核心要点为:

1️⃣线程互斥的核心手段:通过线程锁(重点是互斥量 Mutex)解决共享资源竞争问题,确保同一时间只有一个线程访问临界资源。需掌握互斥量的初始化、加锁、解锁及销毁流程,明确锁的生命周期管理。

2️⃣互斥机制的优化与问题:针对线程饥饿(部分线程长期无法获取锁)等问题,需理解其成因并采用合理策略缓解;同时,从底层视角认识互斥量的封装逻辑,有助于深入理解锁的高效性与安全性。

3️⃣线程同步的关键工具:条件变量是实现线程同步的核心机制,与互斥锁配合使用,解决线程间因 “资源状态不满足” 而需等待的场景。需掌握条件变量的等待(wait)、唤醒(signal/broadcast)等函数接口,并通过示例(如生产者 - 消费者模型)理解其 “等待 - 唤醒” 的同步逻辑,尤其注意防范虚假唤醒。

简言之,本文以 “互斥解决竞争,同步协调顺序” 为核心脉络,串联起锁机制、条件变量的使用与原理,为多线程编程的安全性与高效性提供了完整的技术框架。


结束语

以上是我对于【Linux文件系统】线程:线程同步与互斥的理解

感谢您的三连支持!!!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值