Linux线程同步与互斥

目录

一、引言

二、基础概念

三、互斥锁

[3.1 互斥锁的理解](#3.1 互斥锁的理解)

[3.2 互斥锁的接口](#3.2 互斥锁的接口)

四、条件变量

[4.1 条件变量的理解](#4.1 条件变量的理解)

[4.2 条件变量的使用](#4.2 条件变量的使用)

五、生产者消费者模型

六、代码

七、结语


一、引言

在上一篇文章里,我们聊了线程是如何被创建、取消、等待和分离的,这些都挺重要。但是说到线程,还有很重要的一组话题就是线程的同步与互斥。本文将介绍互斥锁、条件变量和生产者消费者模型,最后基于阻塞队列来实现多生产者多消费者模型。

二、基础概念

在学习线程的同步与互斥之前,我们需要理解和线程息息相关的一些概念。

1)临界资源:一次仅允许一个执行流访问的共享资源。

2)临界区:访问共享资源的代码段。

3)互斥:任何时候,互斥保证只有一个执行流进入进入临界区访问临界资源。

4)原子性:不会被任何调度机制打断的操作,做就做完,要么不做。

在多线程并发执行的过程中,临界资源是需要被保护起来的,确保一次只能有一个执行流访问,为的是保护临界资源。互斥锁就起到了保护的作用。

三、互斥锁

3.1 互斥锁的理解

多个执行流在进入临界区前,需要去竞争互斥锁,哪个进程拿到了这把锁,这个进程就可以进入临界区访问共享资源。没有竞争到锁的线程就会被阻塞,等待锁资源释放后再去竞争锁。

3.2 互斥锁的接口

1)初始化

互斥锁初始化的方式有两种,取决于我们定义的是全局的互斥锁还是局部的互斥锁。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化

  • 通过宏来初始化全局的互斥锁,这个宏不能用来初始化局部的互斥锁,不需要自己手动释放,系统会自己回收。

#include <pthread.h>

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

  • pthread_mutex_t *mutex:指向要初始化的互斥锁的指针。
  • const pthread_mutexattr_t *attr:指向互斥锁属性的指针,一般设为空,表示使用默认属性。
  • 返回值:0表示成功,非0表示错误。

2)销毁

#include <pthread.h>

int pthread_mutex_destroy(pthead_mutex_t *mutex);

  • pthead_mutex_t *mutex:指向要销毁的互斥锁的指针。
  • 返回值:0表示成功,非0表示失败。

3)加锁和解锁

#include <pthread.h>

int pthread_mutex_lock(pthead_mutex_t *mutex);

int pthread_mutex_unlock(pthead_mutex_t *mutex);

  • pthead_mutex_t *mutex:指向互斥锁的指针。
  • 返回值:0表示成功,非0表示失败。

在调用 pthread_mutex_lock函数时,如果没有竞争到锁,那么该进程就会被阻塞。

四、条件变量

4.1 条件变量的理解

条件变量(Condition Variable)是一种同步机制,用于线程间的通信。它允许线程在某个条件不满足时进入等待状态,直到其他线程通知条件可能已改变。条件变量通常与互斥锁(Mutex)配合使用,因为检测资源是否就绪本身就是在访问共享资源,访问共享资源自然就需要互斥锁来保护,所以我们通常会看到互斥锁和条件变量一起使用。

4.2 条件变量的使用

1)初始化

条件变量的初始化和互斥锁的初始化很类似,也有两种方式。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;/ /静态初始化

  • 用来初始化全局的条件变量或者静态条件变量(static修饰),也不需要自己手动销毁。

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

  • pthread_cond_t *cond:指向要初始化的条件变量的指针。
  • pthread_condattr_t *attr:指向条件变量属性的指针,一般设为空表示使用默认属性。
  • 返回值:0表示成功,非0表示失败。

用来初始化一个局部的条件变量。

2)销毁

int pthread_cond_destroy(pthread_cond_t *cond);

  • pthread_cond_t *cond:指向要销毁的条件变量的指针。
  • 返回值:0表示成功,非0表示失败。

销毁一个条件变量。

3)等待

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

  • pthread_cond_t *cond:指向要等待的条件变量的指针。
  • pthread_mutex_t *mutex:指向互斥锁的指针。
  • 返回值:0表示成功,非0表示失败。

线程一旦调用这个函数,就会到对应的条件变量下等待,等待条件满足,其他线程会唤醒它。这个函数的第二个参数需要解释一下,之所以要把互斥锁传入,是因为不能持有锁就去等待了。这样会导致其他线程得不到锁,一直阻塞,而持有锁的线程有需要其他线程唤醒,这就导致死锁了。这个函数会将传入的互斥锁进行释放,让给其他线程。

4)唤醒

int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

  • pthread_cond_t *cond:指向条件变量的指针。
  • 第一个函数:唤醒一个在该条件变量下等待的线程。
  • 第二个函数:唤醒所有在该条件变量下等待的线程。
  • 返回值:0表示成功,非0表示失败。

至此,线程同步与互斥的手段我们已经明白了。

五、生产者消费者模型

如果没有缓冲区,那么将会是生产者生产数据,然后消费者从生产者处拿数据。这样设计的话,生产者和消费者之间的耦合度很高。如果生产者生产的数据超出了消费者消费的能力,那么生产者只能等待消费者。而加入一个缓冲区以后,只要缓冲区未满,生产者就可以持续生产数据,不用考虑消费者是否来得及消费。这样一来,生产者和消费者之间的耦合度就降低了很多。

上图虽然只画出了一个生产者和一个消费者,但是是支持多生产者多消费者并发的,我们下面的代码也是多生产者多消费者。

六、代码

cpp 复制代码
#ifndef _BLOCKQUEUE_HPP_
#define _BLOCKQUEUE_HPP_

#include <pthread.h>
#include <queue>

const int default_size = 10;//默认大小

class BlockQueue {
public:
    BlockQueue(int size = default_size)
        : _capacity(size)
        , _producter_wait_num(0)
        , _consumer_wait_num(0) 
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_not_full, nullptr);
        pthread_cond_init(&_not_empty, nullptr);
    }
    void push(int data) {
        pthread_mutex_lock(&_mutex);
        while (_queue.size() == _capacity) { //不用if的原因在于防止伪唤醒
            _producter_wait_num++;
            pthread_cond_wait(&_not_full, &_mutex);//生产者等待,等待队列不满
            _producter_wait_num--;
        }
        _queue.push(data);
        if (_consumer_wait_num > 0) {
            pthread_cond_signal(&_not_empty);//唤醒消费者
        }
        pthread_mutex_unlock(&_mutex);
    }
    void pop(int &data) {
        pthread_mutex_lock(&_mutex);
        while (_queue.empty()) {//不用if的原因在于防止伪唤醒
            _consumer_wait_num++;
            pthread_cond_wait(&_not_empty, &_mutex);//消费者等待,等待队列不为空
            _consumer_wait_num--;
        }
        data = _queue.front();
        _queue.pop();
        if (_producter_wait_num > 0) {
            pthread_cond_signal(&_not_full);//唤醒生产者
        }
        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue() {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_not_full);
        pthread_cond_destroy(&_not_empty);
    }
private:
    std::queue<int> _queue;
    pthread_mutex_t _mutex;
    pthread_cond_t _not_full;
    pthread_cond_t _not_empty;
    int _capacity;//队列容量
    int _producter_wait_num; // 生产者等待的数量
    int _consumer_wait_num; // 消费者等待的数量
};

#endif // _BLOCKQUEUE_HPP_

运行程序后,你将看到生产者线程和消费者线程的输出,验证阻塞队列的正确性。生产者线程会依次将数据放入队列,消费者线程会依次从队列中取出数据。由于多线程的并发性,输出顺序可能不固定,但数据的生产与消费应该是匹配的。

七、结语

"路漫漫其修远兮,吾将上下而求索。"

上一篇链接:https://blog.csdn.net/bit_pan/article/details/151579387?spm=1001.2014.3001.5502


完~

相关推荐
LeaderSheepH2 小时前
Java自定义比较器详解
java·开发语言
七夜zippoe2 小时前
缓存三大劫攻防战:穿透、击穿、雪崩的Java实战防御体系(二)
java·开发语言·缓存
和编程干到底2 小时前
Linux中进程和线程常用的API详解
linux·运维·服务器
大飞pkz2 小时前
【设计模式】题目小练1
开发语言·设计模式·c#·题目小练
小猪写代码2 小时前
Ubuntu C编程 (make工具和Makefile的引用)
linux·运维·ubuntu
肖爱Kun2 小时前
LINUX中USB驱动架构—设备驱动
linux·驱动
FriendshipT3 小时前
Nuitka 将 Python 脚本封装为 .pyd 或 .so 文件
开发语言·python
白鹭3 小时前
apache实现LAMP+apache(URL重定向)
linux·运维·apache·url重定向·apache实现lamp架构
她说人狗殊途3 小时前
动态代理1
开发语言·python