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,表示锁现在可用
- 唤醒可能正在等待该锁的一个或多个线程
- 允许等待中的线程继续执行