Linux 线程同步与互斥详解(含 C++ 代码示例)
详细讲解了 Linux 环境下线程同步与互斥的核心概念。内容包括互斥锁的定义、实现原理及防止数据竞争的方法,分析了死锁产生的必要条件与避免策略。重点介绍了条件变量在解决线程饥饿问题中的应用,并通过生产者 - 消费者模型演示了如何结合互斥锁与条件变量实现阻塞队列。此外还涵盖了信号量的基本用法。文中提供了完整的 C++ 代码示例,包括锁的封装、任务对象化及多线程协作逻辑,旨在帮助开发者掌握 Linux 多线程编程的关键技术。

详细讲解了 Linux 环境下线程同步与互斥的核心概念。内容包括互斥锁的定义、实现原理及防止数据竞争的方法,分析了死锁产生的必要条件与避免策略。重点介绍了条件变量在解决线程饥饿问题中的应用,并通过生产者 - 消费者模型演示了如何结合互斥锁与条件变量实现阻塞队列。此外还涵盖了信号量的基本用法。文中提供了完整的 C++ 代码示例,包括锁的封装、任务对象化及多线程协作逻辑,旨在帮助开发者掌握 Linux 多线程编程的关键技术。

在程序中部分资源是共享的,如全局变量所有线程都能访问。当多个线程同时访问这种共享资源时,就可能导致数据不一致。
解决方法:
概念:
对于 C/C++ 代码会经过编译变成汇编,而某些代码的底层可能不止一条汇编语句,肯定不是原子的。例如变量 a++ 的底层过程:

因为时间片切换,可能在一个代码底层的多个语句中的第二个语句时间片就到了,导致未执行完就切走了。轮到执行其他线程,假设其他线程也同样访问相同的内存资源,并修改了该资源。当后面时间片到了,又轮到一开始的线程,他会继续从未完成的部分开始,而非先读取内存数据,这样就会导致前面的线程的任务白做(因为一开始的线程的数据是之前的数据,他修改后就会导致数据回到之前)!
多线程并发访问全部整形的汇编,若不是原子的,就会有数据不一致的并发问题。
认识到 CPU 中执行过程:
其中 if 判断语句同样不是原子的(本质也和上面的 ++ 一样需要到 CPU 中去处理判断,这样汇编语句就有多个)。数据在内存中,本质是被线程共享的,数据被读取到寄存器汇总,本质变成了线程的上下文,它是属于线程私有数据。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER,此时定义的全局锁就能直接使用,并且不用销毁。int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr),attr 是锁的属性,暂不考虑写成 nullptr。int pthread_mutex_destroy(pthread_mutex_t *mutex)。当在局部创建一把锁时,需要对其进行初始化操作:
pthread_mutex_init、pthread_mutex_destroy,也就代表使用完后需要摧毁。因为自定义的函数会使用到锁所以需要把它当参数传递进去。
pthread_mutex_lock(pthread_mutex_t* mutex)pthread_mutex_unlock(pthread_mutex_t* mutex)申请成功返回 0,失败返回错误码。
注意事项:
根据互斥的定义:任何时刻只允许一个线程申请锁成功!其他线程申请失败时,会在 mutex 锁上进行阻塞,本质也就是等待。
推出了不阻塞的锁:
5. 不阻塞的申请锁:pthread_mutex_trylock(pthread_mutex_t* mutex)
线程在临界区中访问加锁的临界资源的时候是可能发生切换,但即便切换了别人也仍然不能访问。(理解成,一个房间只要有钥匙才能进入,而当我们进入后我们出去时是拿着钥匙走的,别人同样还是进不去)
// main.cpp
#include <iostream>
#include <thread>
#include <cstdlib>
#include "LockGuard.hpp"
#include "Thread.hpp"
#include <unistd.h>
#include <vector>
#include <string>
using namespace std;
// 应用方的视角
// 为了能同时传递线程名和锁变量
// 就构建 ThreadData 类,让其可以直接一起传递进去
class ThreadData {
public:
ThreadData(string& name, pthread_mutex_t* lock) : threadname(name), pmutex(lock) {}
public:
string threadname;
pthread_mutex_t* pmutex;
};
void Print(int num) {
while (num) {
std::cout << "hello world: " << num-- << std::endl;
sleep(1);
}
}
int ticket = 10000; // 全局的共享资源
// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //全局锁,不用 init 初始化
void GetTicket
{
() {
{
;
(ticket > )
{
();
(, td->threadname.(), ticket);
ticket--;
}
{
;
}
}
}
}
{
number = ;
name[];
(name, (name), , number++);
name;
}
{
mutex;
(&mutex, );
string name1 = ();
ThreadData* td1 = (name1, &mutex);
;
string name2 = ();
ThreadData* td2 = (name2, &mutex);
;
string name3 = ();
ThreadData* td3 = (name3, &mutex);
;
string name4 = ();
ThreadData* td4 = (name4, &mutex);
;
t();
t();
t();
t();
t();
t();
t();
t();
(&mutex);
td1;
td2;
td3;
td4;
;
}
// Thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
using namespace std;
// typedef function<void()> func_t
// 注意!!
// 此处的修改,因为带有模板所以后面用该类型变成 fun_t<T>
template<class T>
using func_t = function<void(T)>; // 返回值 void 参数为;加上模板,这个类型是给传进来的参数数据 data 的!
template<class T>
class Thread {
public:
Thread(T data, func_t<T> func, const string& name) : _tid(0), _name(name), _isrunning(false), _func(func), _data(data) {}
// 因为在类内的函数默认是有 this 指针的这样就会导致 pthread_create 的 threadroutine 的类型不匹配而导致的无法传参
// 所以解决方法就是改成静态函数,但此时又不能使用成员变量了,所哟把参数 args 改成 this 传递进来!
static void* ThreadRoutine(void* args) {
Thread* ts = static_cast<Thread*>(args);
ts->_func(ts->_data);
return nullptr;
}
bool Start {
n = (&_tid, , ThreadRoutine, );
(n == ) {
_isrunning = ;
;
}
;
}
{ _name; }
{
(!_isrunning) ;
n = (_tid, );
(n == ) {
_isrunning = ;
;
}
;
}
{ _isrunning; }
~() {}
:
string _name;
<T> _func;
_tid;
_isrunning;
T _data;
};
LockGuard.hpp:
#pragma once
#include <pthread.h>
class Mutex {
public:
Mutex(pthread_mutex_t* lock) : _lock(lock) {}
void Lock() { pthread_mutex_lock(_lock); }
void Unlock() { pthread_mutex_unlock(_lock); }
~Mutex() {}
private:
pthread_mutex_t* _lock; // 不定义锁,默认外面会创建传进来
};
class LockGuard {
public:
LockGuard(pthread_mutex_t* lock) : _mutex(lock) { _mutex.Lock(); }
~LockGuard() { _mutex.Unlock(); }
private:
Mutex _mutex;
};
上述代码就能实现一个基本的抢票机制,通过互斥锁就不会导致数据错乱。
但有可能一个票被一个人全部都抢了,对其他线程就形成了饥饿问题,他的原理是一个线程在申请完锁,解锁后又立马申请锁资源。
要解决饥饿问题,互斥是无法解决的,也就是同步解决:当一个线程申请完锁后不能再立马申请锁资(也就是有一定的顺序性),请看目录中的同步。
大多数体系结构都提供了 swap/exchange 指令,该指令的作用是把寄存器和内存单元的数据相交换。
底层汇编原理:
exchange eax mem_addr(也就是把寄存器 eax 的内容和内存数据 mem_addr 交换的汇编语句),因为只有一条主要语句所以交换的过程是原子的,其他语句执行时即使被切换,也会把自身数据(%al 寄存器的数据)带走,也就不会影响其他线程。
互斥锁的实现原理:

解释:
每个线程要执行加锁时首先都会先 move 0 到 %al 寄存器中,然后在和 mutex 的数据进行交换,然后判断寄存器 %al 中的值>0 则表示加锁成功,反之则加锁失败在加锁处挂起等待! 加锁失败的情况,因为有人已经在使用该锁,所以会把内存中的 mutex 改为了 0(他也会执行第一个 move 操作),当他没有归还锁资源时,mutex 中的值为 0,当别人一交换则 %al 寄存器中就会变成 0 就会挂起等待了。(这样也就实现了互斥锁,加锁的区域只能由一个线程使用!)
所以加锁的本质就是:
线程安全:
线程不安全的情况:
线程安全的情况:
常见不可重入的情况:
常见可重入的情况:
可重入与线程安全联系:
可重入与线程安全区别:
原理: 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。 可以理解成,现在用了两把锁 A/B 各用了一把锁,但两个线程必须同时持有两把锁才能继续往后运行(如下图)。

一个线程也可能死锁:在程序内部重复申请已经申请过的锁(此时申请不到就会被永久的挂起了)。
避免死锁的算法:死锁检测算法,银行家算法。
背景: 当一个线程短时间的不断的申请锁,释放锁,导致其他人长时间得不到资源,也就对其他线程产生饥饿问题,为了解决饥饿问题,归还资源后线程不能立即再次申请,再通过类似队列的结构(先进先出,也就是有顺序性)管理要申请锁资源的线程。
对此解决这个问题的方法就是同步:
同步:在临界资源使用安全的前提下,让多线程执行具有一定的顺序性。互斥能保证资源的安全,同步能够较为充分高效的使用资源。
(计算机领域非常重要的模型)

日常生活中超市就是典型的生产者消费者模型就有生产者、消费者(如下图):

生产者、消费者的三大关系:
生产者消费者模型本质就是处理好上方的三个关系。

321 原则:
3. 三种关系 2. 两种角色(生产线程/消费进程)
- 一个交易场所(内存空间)
同步的实现就是通过:条件变量(互斥中有锁一样)。

条件变量是类似于铃铛提醒人的工具,这个条件变量是为了避免消费者(线程)在资源还没准备好就不断的去内存申请但也拿不到资源的情况(本质就是同步的工具),因为共享空间是互斥的,这样就会导致生产者也无法放数据(饥饿)。
条件变量:当生产者将资源准备好去提醒消费者。
也就相当于条件变量的结构为:
struct cond {
// 条件是否就绪
int flag;
// 维护一个线程队列 tcb_queue
}()
当 flag 表示就绪就从线程队列中唤醒一个线程。
// 对局部条件变量摧毁
int pthread_cond_destroy(pthread_cond_t *cond);
// 对局部条件变量进行初始化
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
// 全局的条件变量,和锁一样可以直接使用
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 在指定的条件变量 cond 处等待,并且还要传递一把锁 mutex
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 唤醒所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* threadRoutine(void* args) {
string name = static_cast<const char*>(args);
while (true) {
// sleep(1);
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex); // 等待
cout << "I am a new thread:" << name << endl;
pthread_mutex_unlock(&mutex);
}
}
// 主线程
int main() {
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, threadRoutine, (void*)"thread-1");
pthread_create(&t2, nullptr, threadRoutine, (void*)"thread-2");
pthread_create(&t3, nullptr, threadRoutine, (void*)"thread-3");
sleep(5); // 5s 后让条件变量唤醒线程
while (true) {
(&cond);
();
}
(t1, );
(t2, );
(t3, );
}
当只唤醒一个线程(pthread_cond_signal)时他是逐个的:

当唤醒全部线程 (pthread_cond_broadcast) 时他是所有线程一起的:

从上面的饥饿问题不能发现:单纯的互斥,能保证数据的安全,但不一定合理或高效。
pthread_cond_wait 函数的细节:
下面将使用到:
原理:
生产者将一个任务 push 到队列中,而消费者再去通过 pop 得到数据并处理。
阻塞队列:
// BlockQueue.hpp
#pragma once
#include <iostream>
#include <queue>
#include <ctime>
#include <unistd.h>
#include <pthread.h>
#include "LockGuard.hpp"
using namespace std;
const int defaultcap = 5;
template<class T>
class Blockqueue {
public:
Blockqueue(int cap = defaultcap) : _capacity(cap) {
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_c_cond, nullptr);
pthread_cond_init(&_p_cond, nullptr);
}
bool IsFull() { return _q.size() == _capacity; }
bool IsEmpty() { return _q.size() == 0; }
// 生产者
void Push {
;
(()) {
(&_p_cond, &_mutex);
}
_q.(in);
(&_c_cond);
}
{
;
(()) {
(&_c_cond, &_mutex);
}
*out = _q.();
(&_p_cond);
_q.();
}
~() {
(&_mutex);
(&_c_cond);
(&_p_cond);
}
:
queue<T> _q;
_capacity;
_mutex;
_p_cond;
_c_cond;
};
封装锁:
// LockGuard.hpp
#pragma once
#include <pthread.h>
class Mutex {
public:
Mutex(pthread_mutex_t* lock) : _lock(lock) {}
void Lock() { pthread_mutex_lock(_lock); }
void Unlock() { pthread_mutex_unlock(_lock); }
~Mutex() {}
private:
pthread_mutex_t* _lock; // 不定义锁,默认外面会创建传进来
};
class LockGuard {
public:
LockGuard(pthread_mutex_t* lock) : _mutex(lock) { _mutex.Lock(); }
~LockGuard() { _mutex.Unlock(); }
private:
Mutex _mutex;
};
任务对象:
// Task.hpp
#pragma once
#include <iostream>
enum { ok = 0, div_zero, mod_zero, unknow };
class Task {
public:
Task() {}
Task(int x, int y, char op) : _x(x), _y(y), _oper(op) {}
void Run() {
switch (_oper) {
case '+': result = _x + _y; break;
case '-': result = _x - _y; break;
case '*': result = _x * _y; break;
case '/': {
if (_y == 0) {
code = div_zero; break;
}
result = _x / _y;
}
break;
case '%': {
if (_y == 0) {
code = mod_zero; break;
}
result = _x % _y;
}
break;
default: code = unknow; break;
}
}
string PrintTask() {
string s;
s += to_string(_x);
s += _oper;
s += to_string(_y);
s += ;
s;
}
{ (); }
{
string s;
s += (_x);
s += _oper;
s += (_y);
s += ;
s += (result);
s += ;
s += (code);
s += ;
s;
}
~() {}
:
_x;
_y;
_oper;
result;
code;
};
主程序:
// main.cc
#include <iostream>
#include <queue>
#include <ctime>
#include <unistd.h>
#include <pthread.h>
#include "LockGuard.hpp"
using namespace std;
const int defaultcap = 5;
template<class T>
class Blockqueue {
public:
Blockqueue(int cap = defaultcap) : _capacity(cap) {
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_c_cond, nullptr);
pthread_cond_init(&_p_cond, nullptr);
}
bool IsFull() { return _q.size() == _capacity; }
bool IsEmpty() { return _q.size() == 0; }
// 生产者
void Push(const T& in) {
;
(())
{
(&_p_cond, &_mutex);
}
_q.(in);
(&_c_cond);
}
{
;
(())
{
(&_c_cond, &_mutex);
}
*out = _q.();
(&_p_cond);
_q.();
}
~() {
(&_mutex);
(&_c_cond);
(&_p_cond);
}
:
queue<T> _q;
_capacity;
_mutex;
_p_cond;
_c_cond;
};
对此为什么生产者消费者模型能提高数据处理的效率?
因为对于生产者消费者模型来说,我们不能只看内部他的生产和消费过程,这里是互斥的并没有效率的提升,但是从整体来看生产者线程获取数据的过程和消费者线程处理数据的过程也是需要时间的,他们在处理这些时间时,其他线程就能同步的去执行生产/消费过程,从而实现每个线程都能高效的执行其作用。
该方法同样也是实现同步的工具(只不过常用条件变量,所以这里就简略了)。
在之前文章中已经写过信号量的基本概念有:
PV 操作是原子的!

把公共资源不当做整体,多线程不访问临界资源的同一个区域。 对此信号量为了防止分成的 n 份公共资源,分给了 n+k 个线程,信号量的作用就是:确定线程能否访问资源。 线程信号量申请成功后,当线程需要使用时就不需要再判断资源是否就绪,直接就能使用了(申请信号量时已经判断了)。
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 销毁信号量
int sem_destroy(sem_t *sem);
信号量的 PV 操作
int sem_wait(sem_t *sem);
申请成功继续,失败则阻塞等待。
int sem_post(sem_t *sem);
上述函数的返回值都是:成功为 0,失败为非 0 错误码。
本章内容涵盖 Linux 下线程同步与互斥的核心机制,包括互斥锁原理、死锁避免、条件变量实现生产者 - 消费者模型及信号量基础。通过 C++ 代码示例展示了关键 API 的使用与封装技巧,帮助开发者构建安全高效的多线程程序。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online