《内核视角下的 Linux 锁与普通生产消费模型:同步原语设计与性能优化思路》

**前引:**在 Linux 系统中,并发是常态,但并发带来的竞争条件、数据不一致问题,全靠 "锁" 来兜底。从内核态的进程调度到用户态的多线程编程,锁是保障系统稳定的核心同步原语。本文将跳出 "只会用" 的层面,深入内核源码逻辑,拆解自旋锁、互斥锁等常见锁的实现机制,剖析不同锁的设计取舍,帮你从底层理解 Linux 锁的工作原理与性能关键!

目录

【一】共享数据不一致问题

(1)不一致现象

(2)加锁理解

【二】互斥"锁"

(1)定义锁变量

(2)初始化锁

(3)获取锁

(4)主动释放锁+获取返回值

(5)自动释放进程资源(无需获取返回值)

(6)释放互斥锁

【三】"死锁"

【四】条件"锁"+互斥"锁"

(1)定义锁变量

(2)初始化锁

(3)阻塞等待队列

(4)唤醒等待队列线程

阻塞等待理解:

(5)释放条件锁

【五】"生产消费"模型

(1)理论认识

(2)理论框架

(3)生产实现

(4)消费实现

(5)效果展示:

两大隐藏点讲解:


【一】共享数据不一致问题

(1)不一致现象

我们先来看一段代码,如果现在有一个全局整数,由3个线程并发减减,减到0结束,看看结果:

理想效果:变量不管如何变化,减到0应该会结束所有线程访问,但是下面是测试结果:

原因解释:

数据需要从内存拿到CPU运算得到处理结果:但是如果一个线程的时间片到了,它只完成了第一步(加载到内存)此时从CPU拿回数据结果为10,但是CPU已经完成了运算10->9,CPU返回给内存。此时线程1再次调用运算,给CPU的数据就是10,虽然看上去是这个时间片的问题,但是数据每次执行运算都是根据(date>0)判断,所以只要同步这个数据结果,就不会出现这个不一致问题

(2)加锁理解

上面出现的问题无非是:共享资源被并发线程访问出现的数据不一致问题,而"锁"恰好可以解决:

**"锁"的理解:**进程或线程操作同一个资源(比如文件、内存数据、设备)时,锁会强制它们 "排队访问",确保同一时间只有一个执行者能操作资源,从而避免冲突(类似队列式访问)
"锁"的特点:

互斥性:同一时间,锁只能被一个进程 / 线程持有,其他请求者会被阻塞或直接返回失败

原子性:锁的 "获取" 和 "释放" 操作是不可分割的,不会出现 "一半获取成功" 的中间状态
锁的使用:对"获取锁"和"释放锁"范围的代码形成加锁,即中间代码每次只允许一个线程访问

【二】互斥"锁"

什么是互斥"锁"?单单的满足你访问我就不能访问的条件,每次只有一个执行流访问

(1)定义锁变量

在使用锁之前我们需要先定义一个锁变量:

cpp 复制代码
pthread_mutex_t 变量名; // 定义锁变量

例如:

或者直接全局初始化一把锁,就可以直接使用:直接开始(3)和(4)

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

pthread_mutex_t 锁变量名 = PTHREAD_MUTEX_INITIALIZER;
(2)初始化锁

原型:

cpp 复制代码
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

参数:

**第一个参数:**定义的锁变量地址

**第二个参数:**设置锁的属性,一般传NULL

例如:

(3)获取锁

原型:

cpp 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);

参数:锁变量的地址

例如:

(4)主动释放锁+获取返回值

原型:

cpp 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数:锁变量的地址

例如:

锁的使用举例:

对我们刚才的代码进行加锁,看是否还能出现数据不一致的问题:需要注意释放锁的位置

运行结果:

(5)自动释放进程资源(无需获取返回值)

原型:

cpp 复制代码
#include <pthread.h>
int pthread_detach(pthread_t thread);

参数:要自动释放资源的线程 ID

作用:让线程结束后自动回收资源

(6)释放互斥锁

原型:

cpp 复制代码
pthread_mutex_destroy(互斥锁地址);

【三】"死锁"

"死锁"我们简单了解即可,需要同时满足下面四个条件才是"死锁":

(1)互斥条件:一个资源每次只能被一个执行流使用

(2)请求与保持条件:一个执行流因请求资源而阻塞时,对已获取的资源保持不放

(3)不剥夺条件:一个执行流已获取的资源,在未使用之前,不能强行剥夺

(4)循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

【四】条件"锁"+互斥"锁"

什么是条件"锁"?尝尝用于需要"等待"条件的情况,需要以互斥"锁"为底层锁基础

(1)定义锁变量

原型:

cpp 复制代码
pthread_cond_t cond  条件锁变量名

参数:条件锁变量名

或者直接全局初始化一把锁,就可以直接使用:直接开始(3)和(4)

cpp 复制代码
pthread_cond_t 条件锁变量名 = PTHREAD_COND_INITIALIZER

例如:使用互斥"锁"+条件"锁"的初始化

(2)初始化锁

单独初始化:

cpp 复制代码
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

参数:

**第一个参数:**条件锁变量的地址

**第二个参数:**属性选择,一般为NULL

(3)阻塞等待队列

原型:

cpp 复制代码
int pthread_cond_wait(pthread_cond_t *restrict cond, 
                   pthread_mutex_t *restrict mutex);

参数:

**第一个参数:**条件变量指针

**第二个参数:**关联的互斥锁指针,调用前必须持有该锁

作用:使当前线程阻塞,等待条件变量被唤醒;

自动释放传入的互斥锁(因此前提需要该线程先持有锁),唤醒后自动重新获取互斥锁

例如:

(4)唤醒等待队列线程

原型:

cpp 复制代码
int pthread_cond_signal(pthread_cond_t *cond);

参数:条件锁的地址

作用:唤醒一个等待该条件变量的线程(通常是等待队列的第一个线程)

例如:

阻塞等待理解:

每个线程会按照顺序进入类似队列的结构中,待被唤醒,达到先进先出的效果,例如:

1<---2<---3依次进入等待队列,每次调用被唤醒一个线程,随即自动给改进程加互斥锁

(5)释放条件锁

原型:

cpp 复制代码
pthread_cond_destroy(条件锁地址);

【五】"生产消费"模型

(1)理论认识

假设当前存在:消费者、供应商(供货商)、超市三种关系组成

超市作为中介解决了"消费者"直接面向"供应商"完全不协调的问题:

消费者不定时的消费商品,供应商每次单个生产货物,超市作为存货地,来调节二者生产消费关系

超市:有一定大小的存储空间,商品数量不能超过临界值

消费者:随时取走"超市"的商品,保证每次有商品可被取走,不能出现并发"消费"

供应商:可根据"超市"的存储空间来不断供货,保证货物不超出存储空间,不能出现并发"供货"

(注意:也不能出现并发的"消费"和"供应",因为可能因为执行时差导致失败)

从线程角度来说:

"生成消费"模型是通过建立中间数据缓存区(阻塞队列)来实现对数据生成与处理的解耦

(解耦:降低对象之间的依赖关系,不至于一方崩了和它有关联的都崩了........)

(2)理论框架

"消费者"与"生产者"都是通过"锁"的控制来控制多线程并发操作的问题,这里以队列为中间存储区

既然不能并发"消费",不能并发"供应",不能并发"消费"和"供应",因此------>只有一个互斥"锁"

而"消费"和"供应"又是解耦的,所以------>存在两个条件"锁"

那么类的设计就出来了:

cpp 复制代码
template<class T>
class Pthread_P_C
{
public:
    Pthread_P_C()
    {
        //初始化互斥锁
        pthread_mutex_init(&mute,NULL);
        //初始化条件锁
        pthread_cond_init(&cond_p,NULL);
        pthread_cond_init(&cond_c,NULL);
    }
    ~Pthread_P_C()
    {
        //释放互斥锁
        pthread_mutex_destroy(&mute);
        //释放条件锁
        pthread_cond_destroy(&cond_p);
        pthread_cond_destroy(&cond_c);
    }

    //生产
    void push_back(const T& date)
    {

    }

    //消费
    void pop()
    {
        
    }

private:
    //缓存
    std::queue<T> _buffer;
    //互斥锁
    pthread_mutex_t mute;
    //条件锁*2
    pthread_cond_t cond_p;
    pthread_cond_t cond_c;
};
(3)生产实现

理论:每次生产会调用一次push_back(),因此存在两种情况:当然都是在互斥条件下执行

(1)如果缓冲区满了,那就阻塞等待无法生产,可以尝试给"消费"发信号:你赶紧来消费

(2)如果缓冲区没满,那就生产,然后告诉"消费":你可以继续"消费"了

cpp 复制代码
//生产
    void push_back(const T& date)
    {
        //互斥
        pthread_mutex_lock(&mute);
        //如果缓冲区满了
        while(_buffer.size()>=MAX)
        {
            std::cout<<"你赶紧给我过来消费!"<<std::endl;
            //给消费发信号
            pthread_cond_signal(&cond_c);
            ////以防万一继续阻塞
            pthread_cond_wait(&cond_p,&mute);
        }

        //说明可以生产
        std::cout<<"生产成功:"<<date<<std::endl;
        _buffer.push(date);
        //继续消费
        pthread_cond_signal(&cond_c);

        //解互斥锁
        pthread_mutex_unlock(&mute);
    }
(4)消费实现

理论:每次生产会调用一次pop(),因此存在两种情况:当然都是在互斥条件下执行

(1)如果缓冲区为空,说明无法消费,可以尝试告诉"消费":催促生产!

(2)如果缓冲区有数据,说明可以消费,然后告诉"生产":你可以继续"生产"了

cpp 复制代码
//消费
    void pop()
    {
        //互斥
        pthread_mutex_lock(&mute);
        //如果为空
        while(_buffer.size()==0)
        {
            std::cout<<"你给我赶紧生产!"<<std::endl;
            //给生产发信号
             pthread_cond_signal(&cond_p);
            //以防万一继续阻塞
            pthread_cond_wait(&cond_c,&mute);
        }

        //说明可以消费
        std::cout<<"我消费了:"<<_buffer.front()<<"...."<<std::endl;
        _buffer.pop();
        //继续生产
        pthread_cond_signal(&cond_p);

         //解互斥锁
        pthread_mutex_unlock(&mute);
    }
(5)效果展示:

我们创建两个线程,传实例类的指针来调用生产消费模型,只需无脑调用即可:

cpp 复制代码
#include"pthread_c_p.cpp"

//负责生产
void* Product(void* arg)
{
    //线程结束自动释放资源
    pthread_detach(pthread_self());
    Pthread_P_C<int>* ptr=(Pthread_P_C<int>*)arg;

    int count=P_C;
    while(count--)
    {
        ptr->push_back(count);
    }
    return NULL;
}

//负责消费
void* Consume(void* arg)
{
    //线程结束自动释放资源
    pthread_detach(pthread_self());
    Pthread_P_C<int>* ptr=(Pthread_P_C<int>*)arg;

    int count=P_C;
    while(count--)
    {
        ptr->pop();
    }
    return NULL;
}

int main()
{
    Pthread_P_C<int> V;
    Pthread_P_C<int>* ptr=&V;

    //生产
    pthread_t pd;
    pthread_create(&pd,NULL,Product,ptr);
    //消费
    pthread_t cd;
    pthread_create(&cd,NULL,Consume,ptr);

    sleep(2);
    

    return 0;
}

效果展示:

两大隐藏点讲解:

第一点:效率问题

我们仔细看上面的"生产消费"的实现,不难发现如下图所示的现象:不管多少线程("消费、供应"),每次都只能有一方执行,那么另一方就只能闲着,何谈效率?

"生产消费"模型其实效率的体现不在这里,而在于左右两边对数据的处理工作,只要当前执行流正在"生产"或者"消费"数据,那么其它的所有执行流就可以去随便处理数据,如下图:

第二点:伪进程唤醒问题

明明每个线程每次进入 push 或者 pop 都是该线程单个执行,那么为什么这里用 while 而不是 if ?

用 if 只要它满足阻塞条件就进入阻塞队列,满足条件再被同一个锁唤醒即可,似乎没有BUG!

但是如果多个线程同时进入阻塞队列,此时pthread_cond_signal可能一次性唤醒多个线程,用 if 就会出现问题,而pthread_cond_signal并不保证每次只唤醒一个线程,这和内部有关!

相关推荐
D***t1311 小时前
DevOps技能提升路径
运维·devops
xu_yule1 小时前
Linux_13(多线程)页表详解+轻量级进程+pthread_create
linux·运维·服务器
拾忆,想起1 小时前
Dubbo动态配置实时生效全攻略:零停机实现配置热更新
分布式·微服务·性能优化·架构·dubbo
江湖有缘3 小时前
Linux系统之htop命令基本使用
linux·运维·服务器
CodeByV3 小时前
【Linux】基础 IO 深度解析:文件、描述符与缓冲区
linux
B***y8853 小时前
配置nginx访问本地静态资源、本地图片、视频。
运维·nginx
w***Q3506 小时前
Git工作流自动化
运维·git·自动化
xu_yule9 小时前
Linux_12(进程信号)内核态和用户态+处理信号+不可重入函数+volatile
linux·运维·服务器
虾..9 小时前
Linux 环境变量&&进程优先级
linux·运维·服务器