《内核视角下的 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并不保证每次只唤醒一个线程,这和内部有关!

相关推荐
此生只爱蛋9 小时前
【Linux】正/反向代理
linux·运维·服务器
qq_5470261799 小时前
Linux 基础
linux·运维·arm开发
zfj3219 小时前
sshd除了远程shell外还有哪些功能
linux·ssh·sftp·shell
废春啊9 小时前
前端工程化
运维·服务器·前端
我只会发热9 小时前
Ubuntu 20.04.6 根目录扩容(图文详解)
linux·运维·ubuntu
爱潜水的小L9 小时前
自学嵌入式day34,ipc进程间通信
linux·运维·服务器
保持低旋律节奏9 小时前
linux——进程状态
android·linux·php
zhuzewennamoamtf9 小时前
Linux I2C设备驱动
linux·运维·服务器
zhixingheyi_tian10 小时前
Linux 之 memory 碎片
linux
邂逅星河浪漫10 小时前
【域名解析+反向代理】配置与实现(步骤)-SwitchHosts-Nginx
linux·nginx·反向代理·域名解析·switchhosts