Linux:线程同步

线程互斥解决了 "临界资源不冲突" 的问题,但无法保证线程执行顺序。在实际场景中,我们常需要线程按特定逻辑协同工作(如生产者生产数据后,消费者才能消费),这就需要线程同步技术。本文带你理解线程同步的本质、核心工具(条件变量、信号量)及经典应用场景

一、核心问题:为什么需要线程同步?

互斥仅保证 "不冲突",但不保证 "顺序对"。比如生产者线程往队列存数据,消费者线程从队列取数据,若消费者先执行,会取到空数据;若生产者生产速度远快于消费者,队列可能溢出。这种因执行顺序导致的程序异常,称为竞态条件

举个简单例子:两个线程协作,线程 A 输出 "Hello" 后,线程 B 才能输出 "World":

复制代码
// 无同步:执行顺序混乱
#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* threadA(void* arg) {
    sleep(1); // 模拟业务耗时,导致线程B先执行
    std::cout << "Hello ";
    return nullptr;
}

void* threadB(void* arg) {
    std::cout << "World" << std::endl;
    return nullptr;
}

int main() {
    pthread_t tA, tB;
    pthread_create(&tA, nullptr, threadA, nullptr);
    pthread_create(&tB, nullptr, threadB, nullptr);
    
    pthread_join(tA, nullptr);
    pthread_join(tB, nullptr);
    return 0;
}

运行结果可能是 "WorldHello"(顺序颠倒),这就是缺乏同步导致的竞态条件

二、同步核心概念

  • 同步:在保证数据安全的前提下,让线程按特定顺序访问临界资源,避免竞态条件
  • 条件变量:用于线程间 "信号通知",让线程在条件不满足时阻塞,满足时被唤醒
  • 信号量:计数器式同步工具,可用于控制资源访问数量或线程执行顺序
  • 生产者 - 消费者模型:同步的经典应用,通过缓冲区(如队列)解耦生产者和消费者,平衡处理能力

三、核心同步工具 1:条件变量(pthread_cond_t)

条件变量是线程同步的核心工具,需与互斥量配合使用,核心逻辑是 "等待 - 通知" 机制

1. 核心接口

接口函数 功能描述 关键说明
pthread_cond_init(pthread_cond_t* cond, nullptr) 初始化条件变量 默认属性传 nullptr
pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex) 等待条件 自动释放互斥量,被唤醒后重新获取锁
pthread_cond_signal(pthread_cond_t* cond) 唤醒一个等待线程 随机唤醒一个阻塞线程
pthread_cond_broadcast(pthread_cond_t* cond) 唤醒所有等待线程 适合多个线程等待同一条件
pthread_cond_destroy(pthread_cond_t* cond) 销毁条件变量 仅能销毁无线程等待的条件变量

2. 关键疑问:为什么条件变量需要配合互斥量?

条件的判断和修改涉及共享资源(如队列是否为空),必须通过互斥量保护。pthread_cond_wait会原子地执行 "释放锁 + 阻塞",避免 "解锁后、阻塞前" 的窗口期,其他线程修改条件导致信号丢失

3. 同步示例:线程 A 先输出 "Hello",线程 B 再输出 "World"

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

pthread_mutex_t mutex;
pthread_cond_t cond;
bool isHelloPrinted = false; // 共享条件:Hello是否已输出

void* threadA(void* arg) {
    pthread_mutex_lock(&mutex);
    std::cout << "Hello ";
    isHelloPrinted = true; // 修改条件
    pthread_cond_signal(&cond); // 通知线程B:条件满足
    pthread_mutex_unlock(&mutex);
    return nullptr;
}

void* threadB(void* arg) {
    pthread_mutex_lock(&mutex);
    // 循环判断条件(避免虚假唤醒)
    while (!isHelloPrinted) {
        pthread_cond_wait(&cond, &mutex); // 条件不满足,阻塞等待
    }
    std::cout << "World" << std::endl;
    pthread_mutex_unlock(&mutex);
    return nullptr;
}

int main() {
    pthread_mutex_init(&mutex, nullptr);
    pthread_cond_init(&cond, nullptr);
    
    pthread_t tA, tB;
    pthread_create(&tA, nullptr, threadA, nullptr);
    pthread_create(&tB, nullptr, threadB, nullptr);
    
    pthread_join(tA, nullptr);
    pthread_join(tB, nullptr);
    
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

运行结果稳定为 "Hello World",通过条件变量实现了线程间的顺序协同。

4. 工程封装:条件变量(Cond.hpp)

结合之前封装的 Mutex,封装条件变量,提升通用性:

复制代码
#pragma once
#include <pthread.h>
#include "Lock.hpp"

namespace CondModule {
using namespace LockModule;

class Cond {
public:
    Cond() {
        pthread_cond_init(&_cond, nullptr);
    }

    ~Cond() {
        pthread_cond_destroy(&_cond);
    }

    // 等待条件(需传入互斥量)
    void Wait(Mutex& mutex) {
        pthread_cond_wait(&_cond, mutex.GetRawMutex());
    }

    // 唤醒一个等待线程
    void Notify() {
        pthread_cond_signal(&_cond);
    }

    // 唤醒所有等待线程
    void NotifyAll() {
        pthread_cond_broadcast(&_cond);
    }

private:
    pthread_cond_t _cond;
};
}

四、核心同步工具 2:POSIX 信号量(sem_t)

信号量是计数器,通过P操作(减 1)和V操作(加 1)实现同步,可用于控制资源访问数量或线程顺序

1. 核心接口

接口函数 功能描述 关键说明
sem_init(sem_t* sem, 0, value) 初始化信号量 第二个参数 0 表示线程间共享,value 为初始计数
sem_wait(sem_t* sem) P 操作:计数减 1 计数为 0 时,线程阻塞
sem_post(sem_t* sem) V 操作:计数加 1 唤醒一个阻塞线程
sem_destroy(sem_t* sem) 销毁信号量 无线程等待时才能销毁

2. 应用示例:控制最多 2 个线程同时访问临界资源

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

sem_t sem; // 信号量:控制并发访问数为2

void* accessResource(void* arg) {
    int threadId = *(int*)arg;
    sem_wait(&sem); // P操作:申请资源,计数减1
    std::cout << "线程" << threadId << " 访问临界资源" << std::endl;
    sleep(2); // 模拟资源占用耗时
    std::cout << "线程" << threadId << " 释放临界资源" << std::endl;
    sem_post(&sem); // V操作:释放资源,计数加1
    return nullptr;
}

int main() {
    sem_init(&sem, 0, 2); // 初始计数2,允许2个线程同时访问
    
    pthread_t t[5];
    int ids[5] = {1,2,3,4,5};
    for (int i=0; i<5; i++) {
        pthread_create(&t[i], nullptr, accessResource, &ids[i]);
    }
    
    for (int i=0; i<5; i++) {
        pthread_join(t[i], nullptr);
    }
    
    sem_destroy(&sem);
    return 0;
}

运行结果中,始终只有 2 个线程同时访问资源,实现了并发数控制。

五、经典同步场景:生产者 - 消费者模型

生产者 - 消费者模型是同步技术的典型应用,通过阻塞队列(缓冲区)解耦生产者和消费者,核心特性:

  • 队列空时,消费者阻塞
  • 队列满时,生产者阻塞
  • 生产者生产数据后,唤醒消费者
  • 消费者消费数据后,唤醒生产者

基于阻塞队列的实现(BlockQueue.hpp)

复制代码
#pragma once
#include <queue>
#include <pthread.h>
#include "Lock.hpp"
#include "Cond.hpp"

namespace SyncModule {
using namespace LockModule;
using namespace CondModule;

template <typename T>
class BlockQueue {
public:
    BlockQueue(int capacity) : _capacity(capacity) {}

    // 生产者入队
    void Enqueue(const T& data) {
        LockGuard lock(_mutex);
        // 队列满时,生产者阻塞
        while (_queue.size() == _capacity) {
            _fullCond.Wait(_mutex);
        }
        _queue.push(data);
        _emptyCond.Notify(); // 唤醒消费者:队列有数据
    }

    // 消费者出队
    T Dequeue() {
        LockGuard lock(_mutex);
        // 队列空时,消费者阻塞
        while (_queue.empty()) {
            _emptyCond.Wait(_mutex);
        }
        T data = _queue.front();
        _queue.pop();
        _fullCond.Notify(); // 唤醒生产者:队列有空间
        return data;
    }

private:
    int _capacity; // 队列最大容量
    std::queue<T> _queue; // 缓冲区队列
    Mutex _mutex; // 保护队列的互斥量
    Cond _fullCond; // 队列满时的条件变量(生产者等待)
    Cond _emptyCond; // 队列空时的条件变量(消费者等待)
};
}

模型测试代码

复制代码
#include "BlockQueue.hpp"
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace SyncModule;

BlockQueue<int> bq(5); // 容量为5的阻塞队列

// 生产者线程:每秒生产一个数据
void* producer(void* arg) {
    int data = 0;
    while (true) {
        bq.Enqueue(data);
        std::cout << "生产者生产:" << data << std::endl;
        data++;
        sleep(1);
    }
    return nullptr;
}

// 消费者线程:每2秒消费一个数据
void* consumer(void* arg) {
    while (true) {
        int data = bq.Dequeue();
        std::cout << "消费者消费:" << data << std::endl;
        sleep(2);
    }
    return nullptr;
}

int main() {
    pthread_t prod, cons;
    pthread_create(&prod, nullptr, producer, nullptr);
    pthread_create(&cons, nullptr, consumer, nullptr);
    
    pthread_join(prod, nullptr);
    pthread_join(cons, nullptr);
    return 0;
}

运行结果中,生产者生产 5 个数据后队列满,进入阻塞;消费者每消费 1 个,生产者唤醒并生产 1 个,完美实现协同。

六、同步的关键注意事项

  1. 避免虚假唤醒pthread_cond_wait可能被系统信号唤醒(虚假唤醒),需用while循环判断条件,而非if
  2. 信号量计数含义:计数为资源可用数量,P 操作申请资源,V 操作释放资源
  3. 同步与互斥的区别:互斥解决 "冲突",同步解决 "顺序";互斥是同步的特例,同步常依赖互斥
  4. 避免过度同步:不必要的同步会导致线程阻塞,降低程序并发性能

下面提供一下代码提供大家测试

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <queue>

template<class T>
class BlockQeueu
{
public:
    BlockQeueu(int capacity = 5)
        : _capacity(capacity)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_full_cond, nullptr);
        pthread_cond_init(&_empty_cond, nullptr);
    }

    ~BlockQeueu()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_full_cond);
        pthread_cond_destroy(&_empty_cond);
    }

    void push(T data) //生产者
    {
        pthread_mutex_lock(&_mutex);
        while (full()) //用while而不是if可以避免虚假唤醒
        {
            std::cout << "我满了!" << std::endl;
            pthread_cond_wait(&_full_cond, &_mutex);
        }
        _queue.push(data);
        pthread_cond_signal(&_empty_cond);
        pthread_mutex_unlock(&_mutex);
    }

    T pop() //消费者
    {
        pthread_mutex_lock(&_mutex);
        while (empty())
        {
            std::cout << "我空了!" << std::endl;
            pthread_cond_wait(&_empty_cond, &_mutex);
        }
        T data = _queue.front();
        _queue.pop();
        pthread_cond_signal(&_full_cond);
        pthread_mutex_unlock(&_mutex);
        return data;
    }
private:
    int _capacity;
    std::queue<T> _queue;
    pthread_mutex_t _mutex;
    pthread_cond_t _full_cond; // _queue满了放入生产者进行等待
    pthread_cond_t _empty_cond; // _queue空了放入消费者进行等待

    bool empty()
    {
        return _queue.size() == 0;
    }

    bool full()
    {
        return _queue.size() == _capacity;
    }
};
cpp 复制代码
#include "BlockQueue.hpp"
#include <unistd.h>

int i = 0;

void* routine_c(void* args)
{
    BlockQeueu<int>* bq = (BlockQeueu<int>*)args;
    while (true)
    {
        bq->push(i++);
        sleep(1);
    }
    return nullptr;
}

void* routine_p(void* args)
{
    BlockQeueu<int>* bq = (BlockQeueu<int>*)args;
    while (true)
    {
        int data = bq->pop();
        std::cout << "我拿到了 : " << data << std::endl;
        sleep(2);
    }
    return nullptr;
}

int main()
{
    BlockQeueu<int>* bq = new BlockQeueu<int>;
    pthread_t c[2];
    pthread_t p[3];
    pthread_create(c, nullptr, routine_c, (void*)bq);
    pthread_create(c + 1, nullptr, routine_c, (void*)bq);
    pthread_create(p, nullptr, routine_p, (void*)bq);
    pthread_create(p + 1, nullptr, routine_p, (void*)bq);
    pthread_create(p + 2, nullptr, routine_p, (void*)bq);
    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(p[2], nullptr);
    return 0;
}
cpp 复制代码
code : Main.cc
	g++ $^ -o $@
.PTHNY : clean
clean : 
	rm -f code
cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <vector>

int NUM = 5;
int cnt = 1000;
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;

void* routine(void* args)
{
    std::string name = (char*)args;
    while (true)
    {
        pthread_mutex_lock(&glock);
        pthread_cond_wait(&gcond, &glock);
        std::cout << "name : " << name << " -> cnt : " << cnt << std::endl;
        cnt--;
        pthread_mutex_unlock(&glock);
    }
    return nullptr;
}

int main()
{
    std::vector<pthread_t> threads;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        char* name = new char[64];
        snprintf(name, 64, "pthread -> %d", i);
        int n = pthread_create(&tid, nullptr, routine, name);
        if (n == 0)
        {
            continue;
        }
        threads.push_back(tid);
    }
    sleep(5);
    while (true)
    {
        /*std::cout << "唤醒一个线程!" << std::endl;
        pthread_cond_signal(&gcond);
        sleep(1);
        */
       std::cout << "唤醒所有线程!" << std::endl;
       pthread_cond_broadcast(&gcond);
       sleep(1);
    }
    for (auto& thread : threads)
    {
        int n = pthread_join(thread, nullptr);
        (void)n;
    }
    return 0;
}
bash 复制代码
CondTest : CondTest.cc
	g++ $^ -o $@
.PTHNY : clean
clean : 
	rm -f CondTest
相关推荐
喵叔哟2 小时前
06-ASPNETCore-WebAPI开发
服务器·后端·c#
Zach_yuan2 小时前
自定义协议:实现网络计算器
linux·服务器·开发语言·网络
岁杪杪2 小时前
关于运维:LINUX 零基础
运维·服务器·php
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [drivers][I2C]I2C
linux·笔记·学习
VekiSon2 小时前
Linux内核驱动——杂项设备驱动与内核模块编译
linux·c语言·arm开发·嵌入式硬件
tianyuanwo2 小时前
企业级NTP客户端配置指南:基于内部NTP服务器的实践
运维·服务器·ntp客户端
芷栀夏2 小时前
CANN开源实战:基于DrissionPage构建企业级网页自动化与数据采集系统
运维·人工智能·开源·自动化·cann
Y1rong2 小时前
linux之网络
linux
寄存器漫游者3 小时前
Linux 软件编程 - IO 编程
linux·运维·spring