当前位置: 首页 > news >正文

Linux锁和互斥锁

        

在 Linux 系统中,​​锁(Lock)​​ 是用于协调多个进程或线程对共享资源(如内存、文件、设备等)访问的同步机制。其核心目标是避免数据竞争(Data Race)和不一致性问题。而 ​​互斥锁(Mutex,Mutual Exclusion Lock)​​ 是锁的一种具体实现形式,专门用于确保同一时刻只有一个线程或进程能访问临界区(Critical Section)。

为什么需要锁?​

1.避免数据竞争(Data Race)

数据竞争​​是并发编程中最常见的问题。当多个线程/进程同时访问同一资源(如内存变量、文件)且至少有一个操作是写操作时,如果没有锁的控制,会导致以下问题:

  • ​覆盖问题​​:两个线程同时修改同一变量,最终结果可能只保留其中一个线程的修改。

int balance = 100;  // 初始余额// 线程A:取款50元
balance = balance - 50;  // 若未加锁,可能被线程B打断// 线程B:存款30元
balance = balance + 30;  // 最终结果可能不是80,而是错误的中间值

 若没有锁,线程A和B的操作可能交替执行,导致 balance 的中间状态被其他线程读取,最终结果错误。

  • ​不可见性问题​​:一个线程对共享变量的修改可能未被其他线程及时感知,导致数据不一致。

2、保证操作的原子性(Atomicity)

​原子性:​​指一个操作要么全部完成,要么完全不执行,不会被其他操作打断。
​锁的作用​​:将一段代码(临界区)标记为“原子操作”,确保执行过程中不会被其他线程干扰。
​示例​​:
银行账户转账操作需要同时完成“扣除A账户金额”和“增加B账户金额”。若没有锁,可能只执行其中一步,导致资金丢失。

3、实现事务的隔离性(Isolation)

在数据库或多线程程序中,​事务​​需要满足ACID特性(原子性、一致性、隔离性、持久性)。锁通过以下方式保障隔离性:

  • ​防止脏读​​:一个事务读取另一个未提交事务的数据(如读取到中间状态)。
  • ​防止幻读​​:事务A读取某范围数据后,事务B插入新数据,导致事务A再次读取时出现“幻影数据”。

​示例​​:MySQL 使用行级锁(如 SELECT ... FOR UPDATE)防止多个事务同时修改同一行数据。

4、协调资源访问顺序

在分布式系统或复杂并发场景中,锁可以定义资源访问的优先级和顺序,避免“饥饿”(某些线程永远无法获取资源)或“死锁”(多个线程互相等待对方释放锁)。

5、保护硬件和操作系统资源

  • ​文件系统​​:多个进程同时写入同一文件可能导致内容混乱,文件锁(如 flock)可确保独占访问。
  • ​内存管理​​:内核通过锁保护页表、进程控制块等关键数据结构。

互斥锁的核心接口

在 C++ 多线程编程中,​​互斥锁(Mutex)​​ 是保护共享资源的核心工具,确保同一时刻只有一个线程访问临界区。

pthread_mutex_int函数

 函数原型:

#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);mutex  指向互斥锁对象的指针(需动态分配内存或静态变量)
attr	  指向互斥锁属性对象的指针(NULL 表示使用默认属性)返回值​​:
0:成功初始化。
非零:错误码(如 EBUSY、EINVAL、ENOMEM 等)。

 mutex 在任何时候只允许一个线程进行资源访问

 锁的初始化与销毁机制:

1. ​​初始化锁​
  • ​静态初始化​​(全局/静态变量):
    通过宏 PTHREAD_MUTEX_INITIALIZER 直接初始化,无需调用函数。

    pthread_mutex_t global_mutex = PTHREAD_MUTEX_INITIALIZER; // 全局锁
    • ​特点​​:编译时完成初始化,生命周期与程序一致。
    • ​适用场景​​:全局锁或静态锁,无需动态调整属性。
  • ​动态初始化​​(局部变量):
    必须调用 pthread_mutex_init() 函数,可自定义属性。

    pthread_mutex_t local_mutex; pthread_mutex_init(&local_mutex, NULL); // 动态初始化
    • ​特点​​:运行时分配内存,需手动管理生命周期。
    • ​适用场景​​:需要自定义锁类型(如递归锁)或动态分配的锁。
2. ​​销毁锁​
  • ​显式销毁​​:调用 pthread_mutex_destroy() 释放锁占用的资源。
    pthread_mutex_destroy(&local_mutex); // 销毁动态锁
    • ​要求​​:锁必须处于未锁定状态。
    • ​作用​​:释放锁内部的数据结构(如等待队列、状态标志)。

全局锁为何不需要显式销毁?

1. ​​生命周期与程序绑定​
  • 全局锁的存储位置是 ​​静态存储区​​(.data 或 .bss 段),其生命周期与程序完全一致。
  • 程序退出时,操作系统会自动回收所有静态资源(包括锁),无需手动释放。
2. ​​静态初始化的特殊性​
  • 静态初始化的锁(如 PTHREAD_MUTEX_INITIALIZER)本质上是 ​​预分配的结构体​​,其内存由编译器自动管理。
  • 销毁操作(pthread_mutex_destroy)主要用于动态分配的锁(如堆上的锁),避免内存泄漏。
3. ​​资源管理的权衡​
  • 全局锁的销毁通常无实际意义,因为程序结束时资源必然释放。
  • 若强制销毁全局锁,需确保程序中所有线程已退出且锁未被持有,否则可能引发未定义行为。

​锁类型​​初始化方式​​是否需要销毁​​原因​
​全局静态锁​PTHREAD_MUTEX_INITIALIZER生命周期与程序绑定,资源由操作系统自动回收。
​动态锁​pthread_mutex_init()需手动管理内存和资源,避免泄漏。
  • 全局锁优先使用静态初始化,避免复杂的销毁逻辑。
  • 动态锁需成对使用 pthread_mutex_init 和 pthread_mutex_destroy,确保资源安全释放。

pthread_mutex_lock 函数

pthread_mutex_lock 是 POSIX 线程库中用于​​互斥锁同步​​的核心函数,其核心作用是确保同一时刻仅有一个线程访问临界区代码。

#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);参数​​:mutex:指向互斥锁对象的指针(需已初始化)。​​返回值​​:
0:成功获取锁。
非零:错误码(如 EBUSY、EINVAL、EDEADLK 等)。

所谓加锁,就是对临界资源进行保护,本质就是对临界区代码进行保护! 

抢票系统代码示例:下面是没有加锁的情况

Thread.cpp文件

#include<iostream>
#include<pthread.h>            //创建线程的头文件
#include<unistd.h>
#include<string>namespace thread
{//线程需要执行方法,调用的回调函数typedef void(*func_t)(const std::string &name);          //函数指针类型class Thread{public://线程执行逻辑void Excute(){std::cout<<_name<<" is runing "<< std::endl;_isruning = true;//调用回调函数,表示哪个线程去调用的,调用完修改当前运行状态_func(_name);_isruning = false; }public:Thread(const std::string &name,func_t func):_name(name),_func(func){std::cout<<"creat "<<name<<" done "<<std::endl;}//线程名字std::string Name(){return _name;}// 线程入口函数 ThreadRoutinestatic void *ThreadRoutine(void *args){Thread *self= static_cast<Thread*>(args);       //self->Excute();return nullptr;}//线程启动方法bool Start()             {int n = ::pthread_create(&_tid,nullptr,ThreadRoutine,this);if(n!=0)return false;return true;}//线程状态std::string Status(){if(_isruning)return "runing";else return "sleep";}//线程终止void Stop(){if(_isruning){::pthread_cancel(_tid);_isruning=false;std::cout<<_name<<" Stop"<<std::endl;}}//等待线程结束,确保主线程与子线程同步void Join(){pthread_join(_tid,nullptr);std::cout<<_name<<" joined"<<std::endl;}~Thread(){}private:std::string _name;                  //线程名字pthread_t _tid;                     //线程idbool _isruning;                     //判断当前状态是否运行func_t _func;                       //线程要执行的回调函数};}

Main.cpp文件

#include<iostream>
#include<pthread.h>            //创建线程的头文件
#include<unistd.h>
#include<vector>
#include<string>
#include"Thread.cpp"
using namespace thread;int tickets =10000;                             //一万张票
void route(const std::string &name)             //抢票系统
{while(true){if(tickets>0){//抢票过程usleep(1000);                       //抢票时间为1msprintf("who: %s,get a ticket: %d\n ",name.c_str(),tickets);tickets--;}else{break;}}
}int main()
{  //创建4个多线程Thread t1("thread -1",route);Thread t2("thread -2",route);Thread t3("thread -3",route);Thread t4("thread -4",route);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();return 0;
}

Makefile文件

test:Main.cppg++ -o $@ $^ -std=c++11 -lpthread         
.PHONY:clean
clean:rm -f test

下面的运行效果:

因为两个线程同时修改同一变量,最终结果可能只保留其中一个线程的修改。实际没有票了,但是线程还在继续跑,导致负数的情况。

在抢票系统中加入全局锁

void route(const std::string &name)             //抢票系统
{while(true){//加锁pthread_mutex_lock(&gmutex);if(tickets>0){//抢票过程usleep(1000);                       //抢票时间为1msprintf("who: %s,get a ticket: %d\n ",name.c_str(),tickets);tickets--;}else{break;}//解锁pthread_mutex_unlock(&gmutex);//这种加锁会导致只有一个线程执行,不合理!}
}

这种加锁方式是不合理的,锁的粒度太大,导致每个线程在处理一次票务之后释放锁,其他线程才能处理。但这样会导致并发性低下,因为每个线程必须等待前一个线程释放锁之后才能处理。另一个更严重的问题是,在检查tickets和修改tickets的过程中,虽然使用了锁来保护,但由于锁的范围包含了usleep,这会导致其他线程在等待锁时无法做任何事情,而当前线程在休眠期间持有锁,这会严重影响性能,导致整体处理速度变慢,但并不会导致只有一个线程执行。

正确的加锁方式

void route(const std::string &name)             //抢票系统
{while(true){//加锁pthread_mutex_lock(&gmutex);if(tickets>0){//抢票过程usleep(1000);                       //抢票时间为1msprintf("who: %s,get a ticket: %d\n ",name.c_str(),tickets);tickets--;pthread_mutex_unlock(&gmutex);}else{pthread_mutex_unlock(&gmutex);break;}      }
}

代码结果演示:

 

这时代码的运行速率明显比上一次要慢一点,但是能够确保每个线程并发运行都是安全的。

 

1.加锁的范围,粒度一定要小

2.任何线程,要进行抢票,都需要申请锁,原则上不应该有例外!

3.所有线程申请锁,前提是所以线程都得看到这把锁,锁本身也是共享资源;加锁的过程必须是原子的!

4.原子性:是并发编程中的一个核心概念,指一个操作要么​​完全执行成功​​,要么​​完全不执行​​,不存在中间状态。换句话说,原子操作是不可分割的,即使在高并发环境下,其他线程也无法观察到该操作的中间步骤。

5.如果申请锁失败了,大多数互斥锁(如 pthread_mutex)默认是阻塞的。

当线程调用 pthread_mutex_lock() 时:

pthread_mutex_lock(&gmutex);  // 若锁被占用,线程在此阻塞
// 临界区操作(访问共享资源)
pthread_mutex_unlock(&gmutex);  // 释放锁,唤醒等待线程
  • ​若锁未被占用​​:线程立即获得锁,继续执行。
  • ​若锁已被占用​​:线程会进入​​等待队列​​,​​阻塞挂起​​,直到锁被释放。

某些锁提供非阻塞的获取方式(如 pthread_mutex_trylock())。此时,申请锁失败会​​立即返回错误​​,而非阻塞线程。

int ret = pthread_mutex_trylock(&gmutex);
if (ret == 0) {  // 成功获取锁// 临界区操作pthread_mutex_unlock(&gmutex);
} else if (ret == EBUSY) {  // 锁已被占用// 处理失败:重试、跳过或执行其他逻辑
}

 6.申请锁成功,继续向后运,执行临界区的代码时,可以被切换

        即使线程成功申请到锁并进入临界区,​​执行临界区代码时仍然可能被操作系统切换​​。但此时锁的持有状态会保持,其他线程无法进入临界区,直到该线程释放锁。

线程切换的底层机制​:

1.​​操作系统的调度权​​:线程的切换由操作系统内核控制,与锁无关。即使线程持有锁,当时间片用完、发生中断(如 I/O 事件)或线程主动让出 CPU(如调用 sleep)时,操作系统仍可能抢占当前线程的执行权。

2.锁的持有状态​​:线程在临界区执行期间,锁的引用计数会增加(如递归锁),或标记为“已锁定”。即使线程被切换,锁的状态仍保持为“被持有”,其他线程无法通过 pthread_mutex_lock() 获取锁。

临界区被切换的影响​:​​

降低并发性​​:其他线程必须等待当前线程释放锁,导致串行执行。

​上下文切换开销​​:频繁的线程切换会增加 CPU 的调度负担。​

​死锁​​:如果临界区内的线程被切换后长时间未释放锁(如陷入死循环或阻塞),其他线程将永久等待。

​逻辑错误​​:若临界区代码未正确处理共享资源(如未释放锁前修改全局状态),可能导致数据不一致。

特性​​说明​
​是否可切换​可以,临界区执行期间线程可能被操作系统调度切换。
​锁的持有状态​线程被切换时,锁仍被持有,其他线程无法进入临界区。
​设计建议​缩小临界区、避免阻塞操作、减少锁持有时间。
​风险​长时间持有锁可能导致性能下降或死锁。

重新设置局部锁

代码:

代码更新区域:创建一个线程参数类,添加线程名称和锁。回调函数调用线程参数td,在线程类里面增加线程参数类的指针作为成员就可以调用锁了!

//线程数据类型,加锁class ThreadData{public:ThreadData(const std::string &name,pthread_mutex_t *lock):_name(name),_lock(lock){}public:std::string _name;              //线程名字pthread_mutex_t *_lock;         //锁};typedef void(*func_t)(ThreadData *td);          //函数指针类型class Thread{public://线程执行逻辑void Excute(){std::cout<<_name<<" is runing "<< std::endl;_isruning = true;//调用回调函数,表示哪个线程去调用的,调用完修改当前运行状态_func(_td);_isruning = false; }public:Thread(const std::string &name,func_t func,ThreadData *td):_name(name),_func(func),_td(td){std::cout<<"creat "<<name<<" done "<<std::endl;}private:std::string _name;                  //线程名字pthread_t _tid;                     //线程idbool _isruning;                     //判断当前状态是否运行func_t _func;                       //线程要执行的回调函数ThreadData*_td;                     //给线程增加参数,参数:锁};}

 

完整Thread.cpp的代码:

#include<iostream>
#include<pthread.h>            //创建线程的头文件
#include<unistd.h>
#include<string>namespace thread
{//线程数据类型,加锁class ThreadData{public:ThreadData(const std::string &name,pthread_mutex_t *lock):_name(name),_lock(lock){}public:std::string _name;              //线程名字pthread_mutex_t *_lock;         //锁};//线程需要执行方法,调用的回调函数typedef void(*func_t)(ThreadData *td);          //函数指针类型class Thread{public://线程执行逻辑void Excute(){std::cout<<_name<<" is runing "<< std::endl;_isruning = true;//调用回调函数,表示哪个线程去调用的,调用完修改当前运行状态_func(_td);_isruning = false; }public:Thread(const std::string &name,func_t func,ThreadData *td):_name(name),_func(func),_td(td){std::cout<<"creat "<<name<<" done "<<std::endl;}//线程名字std::string Name(){return _name;}// 线程入口函数 ThreadRoutinestatic void *ThreadRoutine(void *args){Thread *self= static_cast<Thread*>(args);       //self->Excute();return nullptr;}//线程启动方法bool Start()             {int n = ::pthread_create(&_tid,nullptr,ThreadRoutine,this);if(n!=0)return false;return true;}//线程状态std::string Status(){if(_isruning)return "runing";else return "sleep";}//线程终止void Stop(){if(_isruning){::pthread_cancel(_tid);_isruning=false;std::cout<<_name<<" Stop"<<std::endl;}}//等待线程结束,确保主线程与子线程同步void Join(){pthread_join(_tid,nullptr);std::cout<<_name<<" joined"<<std::endl;}~Thread(){}private:std::string _name;                  //线程名字pthread_t _tid;                     //线程idbool _isruning;                     //判断当前状态是否运行func_t _func;                       //线程要执行的回调函数ThreadData*_td;                     //给线程增加参数,参数:锁};}

完整的Main.cpp代码:

#include<iostream>
#include<pthread.h>            //创建线程的头文件
#include<unistd.h>
#include<vector>
#include<string>
#include"Thread.cpp"
using namespace thread;int tickets =10000;                             //一万张票
void route(ThreadData * td)                     //抢票系统
{while(true){//加锁pthread_mutex_lock(td->_lock);if(tickets>0){//抢票过程usleep(1000);                       //抢票时间为1msprintf("who: %s,get a ticket: %d\n ",td->_name.c_str(),tickets);tickets--;pthread_mutex_unlock(td->_lock);}else{pthread_mutex_unlock(td->_lock);break;}      }
}static int gthreadnum = 4;
int main()
{    //设置局部锁,并初始化pthread_mutex_t mutex;pthread_mutex_init(&mutex,nullptr);//创建4个多线程,用vector管理std::vector<Thread> threads;for (int i = 0; i < gthreadnum; i++){    std::string name = "thread-" + std::to_string(i+1);          //线程名称ThreadData *td = new ThreadData(name,&mutex);                //threads.emplace_back(name,route,td);                         //插入}//执行for (auto &t : threads){t.Start();}for(auto &t:threads){t.Join();}pthread_mutex_destroy(&mutex);                                    //回收锁资源return 0;
}

上述代码运行结果:

 明显没有出现负数情况

在类对象中封装一个RALL风格的锁

LockGuard.cpp

#pragma once
#include<pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){pthread_mutex_lock(_mutex);                      //构造函数里面加锁}~LockGuard(){pthread_mutex_unlock(_mutex);                    //析构的时候唤醒解锁}
private:pthread_mutex_t *_mutex;                             //设置一个锁成员};

Main.cpp代码:

#include<iostream>
#include<pthread.h>            //创建线程的头文件
#include<unistd.h>
#include<vector>
#include<string>#include"Thread.cpp"           //加入自己写的线程类和锁类
#include"LockGuard.cpp"using namespace thread;int tickets =10000;                             //一万张票
void route(ThreadData * td)                     //抢票系统
{while(true){LockGuard Lockguard(td->_lock);         //定义一个临时锁对象,传td指向的锁地址进行加锁(构造时)if(tickets>0){//抢票过程usleep(1000);                       //抢票时间为1msprintf("who: %s,get a ticket: %d\n ",td->_name.c_str(),tickets);tickets--;}else{break;                               //锁对象会在析构的时候自动释放,唤醒解锁}      }
}static int gthreadnum = 4;
int main()
{    //设置局部锁,并初始化pthread_mutex_t mutex;pthread_mutex_init(&mutex,nullptr);//创建4个多线程,用vector管理std::vector<Thread> threads;for (int i = 0; i < gthreadnum; i++){    std::string name = "thread-" + std::to_string(i+1);          //线程名称ThreadData *td = new ThreadData(name,&mutex);                //threads.emplace_back(name,route,td);                         //插入}//执行for (auto &t : threads){t.Start();}for(auto &t:threads){t.Join();}pthread_mutex_destroy(&mutex);                                    //回收锁资源return 0;
}

Thread.cpp文件

 

#include<iostream>
#include<pthread.h>            //创建线程的头文件
#include<unistd.h>
#include<string>namespace thread
{//线程数据类型,加锁class ThreadData{public:ThreadData(const std::string &name,pthread_mutex_t *lock):_name(name),_lock(lock){}public:std::string _name;              //线程名字pthread_mutex_t *_lock;         //锁};//线程需要执行方法,调用的回调函数typedef void(*func_t)(ThreadData *td);          //函数指针类型class Thread{public://线程执行逻辑void Excute(){std::cout<<_name<<" is runing "<< std::endl;_isruning = true;//调用回调函数,表示哪个线程去调用的,调用完修改当前运行状态_func(_td);_isruning = false; }public:Thread(const std::string &name,func_t func,ThreadData *td):_name(name),_func(func),_td(td){std::cout<<"creat "<<name<<" done "<<std::endl;}//线程名字std::string Name(){return _name;}// 线程入口函数 ThreadRoutinestatic void *ThreadRoutine(void *args){Thread *self= static_cast<Thread*>(args);       //self->Excute();return nullptr;}//线程启动方法bool Start()             {int n = ::pthread_create(&_tid,nullptr,ThreadRoutine,this);if(n!=0)return false;return true;}//线程状态std::string Status(){if(_isruning)return "runing";else return "sleep";}//线程终止void Stop(){if(_isruning){::pthread_cancel(_tid);_isruning=false;std::cout<<_name<<" Stop"<<std::endl;}}//等待线程结束,确保主线程与子线程同步void Join(){pthread_join(_tid,nullptr);std::cout<<_name<<" joined"<<std::endl;}~Thread(){}private:std::string _name;                  //线程名字pthread_t _tid;                     //线程idbool _isruning;                     //判断当前状态是否运行func_t _func;                       //线程要执行的回调函数ThreadData*_td;                     //给线程增加参数,参数:锁};}

代码运行结果:

 

 

从原理角度理解锁

互斥锁是一种 ​​二进制同步原语​​,其核心状态只有两种:

  • ​0(未锁定)​​:允许线程获取锁。
  • ​1(锁定)​​:禁止其他线程获取锁。

pthread_mutex_lock() 和 pthread_mutex_unlock() 的实现依赖于 ​​原子操作​​ 和 ​​操作系统内核的同步机制​​(如 futex)。

 

 加锁(lock)操作分析

lock:movb $0, %al       ; 将0存入AL寄存器xchgb %al, mutex   ; 原子性地交换AL寄存器和mutex的值if(al寄存器的内容 > 0){return 0;      ; 成功获取锁,返回0} else {挂起等待;      ; 无法获取锁,线程进入等待状态goto lock;     ; 等待结束后再次尝试获取锁}

工作原理​​:

  • mutex变量作为锁的状态标志:1表示锁可用,0表示锁已被占用
  • xchgb是原子操作指令,确保了整个过程的不可分割性
  • 当线程尝试获取锁时,先用0与mutex交换值
  • 如果交换后AL=1,说明之前mutex=1(锁可用),操作成功
  • 如果交换后AL=0,说明之前mutex=0(锁已被占用),当前线程需等待

 

解锁(unlock)操作分析 

unlock:movb $1, mutex     ; 将1写回mutex变量,表示锁已释放唤醒等待Mutex的线程; 通知等待的线程锁已可用return 0;          ; 返回0表示解锁成功

工作原理​​:

  • 将mutex重置为1,表示锁现在可用
  • 唤醒可能正在等待该锁的一个或多个线程
  • 允许等待中的线程继续执行

 

 

相关文章:

  • 读一本书第一遍是快读还是细读?
  • 【算法专题十五】BFS解决最短路问题
  • 04算法学习_209.长度最小的子数组
  • MCP Server开发使用Pixabay网址搜索图片
  • TypeScript 泛型讲解
  • 《微服务架构设计模式》笔记
  • 基于Matlab建立不同信道模型
  • 鸿蒙HarmonyOS 【ArkTS组件】通用属性-背景设置
  • 腾讯游戏安全与高通合作构建PC端游安全新格局
  • Unity异步加载image的材质后,未正确显示的问题
  • 693SJBH基于.NET的题库管理系统
  • Windows Docker笔记-扩展
  • C++ - 仿 RabbitMQ 实现消息队列(3)(详解使用muduo库)
  • docker面试题(5)
  • 【C++ Primer 学习札记】智能指针
  • selenium——元素定位
  • Java 定时任务中Cron 表达式与固定频率调度的区别及使用场景
  • Unity-编辑器扩展-其二
  • auto关键字解析
  • 【算法】滑动窗口(细节探究,易错解析)5.21
  • 大岭山建设网站/西安百度seo排名
  • 网页源代码在线查看/seo网站优化是什么
  • 网站排名软件优化/正规seo一般多少钱
  • 云南网站开发建设/百度登录入口