【Linux】线程同步

💻文章目录


📄前言

多线程编程互斥机制虽然能保护共享资源的安全,但同时也带来了"线程饥饿问题",而且也无法控制线程的执行顺序,为了解决这种情况,就要用到同步机制。

线程同步

概念

线程同步指的多线程环境中,为了让不同线程按照一定的顺序来执行任务,它和互斥都是为了解决多线程中的并发问题而所存在的机制,要学习同步机制,也得对互斥机制有所了解。

  • 线程互斥与同步的关系:

    • 线程互斥:是为了保证多个线程不会同时访问临界区(共享资源)的机制,但线程的可能会同时竞争锁,无法保证线程执行顺序,

    • 线程同步:在保证线程安全的前提下,让线程按照特定顺序访问资源,从而避免线程饥饿问题。

  • 线程同步的意义:

    • 解决竞态条件: 竞态条件指的是程序因为线程执行先后问题而发生异常,同步机制就用于控制线程顺序。

条件变量

条件变量 是多线程编程中最常见的一种线程同步机制 ,虽然它与互斥量一起使用 ,但它的作用不是锁住线程,而是让线程等待(休眠),直到其他线程发出信号,然后唤醒等待的线程。

函数介绍:

  1. 初始化条件变量
cpp 复制代码
pthread_cond_t // 条件变量的类型(POSIX)

// 函数初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);

// 静态初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 参数:
    • cond:指向被初始化的条件变量
    • attr:指向条件变量的属性对象的指针。可设为NULL
  1. 等待条件变量
cpp 复制代码
// 需要与mutex锁一起使用。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • 参数:
    • cond: 指向需要等待的条件变量的指针,
    • mutex: 指向互斥锁的指针
  • 工作原理: 当线程执行到这条指令时,线程将会休眠,并自动将锁释放 。直到其他线程发出唤醒命令,然后互斥量就会恢复原样
  1. 唤醒线程(条件变量)
cpp 复制代码
// 唤醒单个线程
int pthread_cond_signal(pthread_cond_t *cond);

// 唤醒多个线程
int pthread_cond_broadcast(pthread_cond_t *cond);
  • 参数:
    • cond: 指向条件变量的指针
  1. 摧毁条件变量
cpp 复制代码
int pthread_cond_destroy(pthread_cond_t *cond);
  • 参数:
    • cond: 指向条件变量的指针

函数的使用:

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

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
bool flag = false;
int data = 0;

void* producer(void* arg)
{
    // pthread_detach(pthread_self());
    std::cout << "producer is running:" << std::endl;
    while(data < 100)
    {
        pthread_mutex_lock(&mtx);
        while(flag)    // while循环可防止多个线程同时唤醒时引起虚假唤醒
            pthread_cond_wait(&cond, &mtx);

        data++;	//生产资源
        std::cout << "producer: " << data << std::endl;
        flag = !flag;
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mtx);
    }

    return NULL;
}

void* consumer(void* arg)
{
    // pthread_detach(pthread_self());
    std::cout << "consumer is running:" << std::endl;
    while(data < 100)
    {
        pthread_mutex_lock(&mtx);
        while(!flag)    // flag 和 cond 使用确保了producer先运行
            pthread_cond_wait(&cond, &mtx);
		//使用资源
        std::cout << "consumer: " << data << std::endl;
        flag = !flag;
        pthread_cond_signal(&cond); //唤醒一个线程
        pthread_mutex_unlock(&mtx); 
    }

    return NULL;
}

int main()
{
    pthread_t tid1, tid2;
    try 
    {
    pthread_create(&tid1, NULL, producer, NULL);
    pthread_create(&tid2, NULL, consumer, NULL);
    }
    catch (std::system_error& e)
    {
        std::cerr << e.what() << std::endl;
        return -1;
    }
    pthread_join(tid1, NULL);	// 回收线程资源
    pthread_join(tid2, NULL);

    return 0;
}

生产者-消费者模型

概念

生产者-消费者着模型是多线程编程中用于解决同步问题的一种模式,程序通过分离数据(任务)的产生和消费过程,从而提高程序的性能与可扩展性。

  • 生产者与消费者的关系

    • 生产者与生产者: 互斥 ------ 多个生产者不能同时向缓冲区添加元素。如果一个生产者正在执行添加操作,其他生产者必须等待,直到缓冲区可用。这是为了避免数据冲突和保证数据一致性。
    • 生产者与消费者: 互斥 && 同步 ------ 生产者和消费者需要协调工作的节奏
    • 消费者与消费者: 互斥 ------ 多个消费者不能同时从缓冲区取出元素
  • 生产消费模型优点:

    • 提高并发性
    • 支持生成消费速度不匀
    • 提高代码的可维护性

实现生产者-消费者模型

生产消费模型拥有多种实现方式,有使用基于唤醒队列(信号量)、也有基于阻塞队列(条件变量)的。本文将介绍基于阻塞队列的生产者-消费者模型。

阻塞队列

cpp 复制代码
template <typename T>
class blockqueue
{
public:
    blockqueue(int capacity = 5)	//初始化变量
    :_capacity(capacity), _p_waterline(_capacity - 2), _c_waterline(2)
    {
        pthread_mutex_init(&_mtx, nullptr);
        pthread_cond_init(&_p_cond, nullptr);
        pthread_cond_init(&_c_cond, nullptr);   
    }

    bool empty() const	//判断是否为空
    {
        return _que.empty();
    }

    bool full() const	
    {
        return _que.size() == _capacity;
    }

    void push(const T &data)	//往资源池投放资源。
    {
        LockGuard lock(&_mtx);	//锁住线程
        while (full())	//使用while循环阻止虚假唤醒
        {
            pthread_cond_wait(&_p_cond, &_mtx);
        }
        _que.push(data);
        // 资源数量到底水量线
        if(_que.size() >= _c_waterline) pthread_cond_signal(&_c_cond);       
    }

    T pop()	//取走资源。
    {
        LockGuard lock(&_mtx);
        while (empty())
            pthread_cond_wait(&_c_cond, &_mtx);

        auto& data = _que.front();
        _que.pop();
        if(_que.size() <= _c_waterline) pthread_cond_signal(&_p_cond);

        return data;
    }

    T front()	
    {
        return _que.front();
    }

    ~blockqueue()	// 销毁资源。
    {
        pthread_mutex_destroy(&_mtx);
        pthread_cond_destroy(&_p_cond);
        pthread_cond_destroy(&_c_cond);
    }
private:
    int _capacity;          //队列容量
    int _p_waterline;       //生产者水位
    int _c_waterline;       //消费者水位
    std::queue<T> _que;     //队列
    pthread_mutex_t _mtx;   //互斥锁
    pthread_cond_t _p_cond; //生产者的条件变量
    pthread_cond_t _c_cond; //消费者的条件变量
};

POSIX信号量

概念

POSIX信号量是用于多进程或多线程程序协同共享资源访问的一种同步机制,

工作原理: 信号量是基于等待队列的的结构,当一个线程对值为零的信号量进行 wait 操作时,将会被阻塞,然后被放入等待队列中,直到另一个线程发出 post 信号。

函数介绍

  1. 初始化信号量
cpp 复制代码
// 头文件: <semaphore.h>
// sem_t : 信号量结构

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 参数:
    • sem: 执行信号量结构指针
    • pshared: 如果为非零值,信号量在多个进程间共享;为零则在创建它的进程的各个线程之间共享。
    • value: 信号量的初始值。
  1. 销毁信号量
cpp 复制代码
int sem_destroy(sem_t *sem);
  1. 等待信号量
cpp 复制代码
int sem_wait(sem_t *sem); 
  1. 发布信号量
cpp 复制代码
int sem_post(sem_t *sem);	

函数的使用

cpp 复制代码
sem_t sem; // 定义信号量

// 线程函数1,将等待信号量
void* thread_func_wait(void* arg) {
    printf("Thread 1: Waiting for the semaphore...\n");
    sem_wait(&sem); // 等待信号量
    printf("Thread 1: Received the semaphore signal.\n");
    return NULL;
}

// 线程函数2,将释放信号量
void* thread_func_post(void* arg) {
    printf("Thread 2: Sleeping for 2 seconds before releasing the semaphore...\n");
    sleep(2); // 休眠2秒
    printf("Thread 2: Releasing the semaphore...\n");
    sem_post(&sem); // 释放信号量
    return NULL;
}

int main() {
    pthread_t t1, t2;

    // 初始化信号量,初始值为0
    sem_init(&sem, 0, 0);

    // 创建线程
    pthread_create(&t1, NULL, thread_func_wait, NULL);
    pthread_create(&t2, NULL, thread_func_post, NULL);

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

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

    return 0;
}

其实还有基于信号量的唤醒队列存在,如果感兴趣的话,可以自己动手实现一下。

📓总结

多线程编程中的同步和互斥是为了解决并发访问共享资源造成的竞态条件问题。互斥锁是用于保护资源,而同步是为了控制资源访问的顺序。条件变量和信号量都是实现线程同步的重要工具,它们通过不同的方式控制线程对共享资源的访问。掌握这些基础知识,对于编写高效安全的多线程程序至关重要。

📜博客主页:主页
📫我的专栏:C++
📱我的github:github

相关推荐
J不A秃V头A2 分钟前
Python爬虫:获取国家货币编码、货币名称
开发语言·爬虫·python
憨子周1 小时前
2M的带宽怎么怎么设置tcp滑动窗口以及连接池
java·网络·网络协议·tcp/ip
矛取矛求3 小时前
Linux如何更优质调节系统性能
linux
霖雨3 小时前
使用Visual Studio Code 快速新建Net项目
java·ide·windows·vscode·编辑器
SRY122404193 小时前
javaSE面试题
java·开发语言·面试
Fiercezm3 小时前
JUC学习
java
李元豪3 小时前
【智鹿空间】c++实现了一个简单的链表数据结构 MyList,其中包含基本的 Get 和 Modify 操作,
数据结构·c++·链表
无尽的大道3 小时前
Java 泛型详解:参数化类型的强大之处
java·开发语言
ZIM学编程3 小时前
Java基础Day-Sixteen
java·开发语言·windows
我不是星海3 小时前
1.集合体系补充(1)
java·数据结构