多线程并发编程核心指南:互斥、同步与死锁解决方案详解----《Hello Linux!》(18)
前言
在多核 CPU 普及的今天,多线程并发编程已成为提升程序性能、充分利用硬件资源的核心手段 —— 无论是服务器后台处理、桌面应用响应优化,还是嵌入式设备的任务调度,多线程都扮演着不可或缺的角色。然而,并发带来效率提升的同时,也引入了新的挑战:当多个线程同时访问共享资源(如全局变量、数据库连接)时,极易出现数据竞争(Data Race)、数据不一致等问题,经典的 “抢票问题” 就是典型缩影。更棘手的是,不当的并发控制还可能导致死锁,让程序陷入永久阻塞的困境。
为解决这些问题,“互斥” 与 “同步” 成为多线程编程的两大核心支柱:互斥保证同一时间只有一个线程访问临界资源,从根源上避免数据混乱;同步则让线程按预期顺序执行,实现有序协作。本文将从实际问题出发,系统拆解多线程并发的核心痛点,详细讲解互斥锁(Mutex)的原理与使用、条件变量(Condition Variable)的同步机制,同时深入分析死锁的产生条件与规避方案,还会结合 C++ 实战代码,介绍锁的封装技巧(RAII 风格)、可重入与线程安全的区别等关键知识点。
无论你是刚接触多线程编程的新手,还是想夯实并发基础的开发者,通过本文的学习,都能掌握多线程安全编程的核心逻辑:理解锁的原子性原理、熟练运用 POSIX 线程库的核心接口、规避并发编程中的常见陷阱,最终实现高效、安全的多线程应用开发。
线程互斥
这个问题里面最经典的就是抢票问题
多个线程不停访问这个函数 while (true) { if(tickets > 0) { usleep(1000); printf("who=%s, get a ticket: %d\n", name, tickets); tickets--; } else break; }也就是多个线程并发对一个全局变量
--这个操作是不安全的,会导致数据混乱
原因:
tickets--需要CPU寄存器进行计算这一步的流程:1.将
tickets读到CPU寄存器中 2.计算 3.将计算结果写回内存但是如果一个线程的时间片到了,但是又过了判断,这时就会出问题:
比如:前一个进程是1了,然后判断完断片了;后一个进程就1减为0;然后轮到前一个进程之后又0减为-1
这时就出错了
解决方法:对共享数据的任何访问,需要保证任何时候只有一个执行流访问–也就是互斥
这把锁一般叫做互斥量
加锁的本质:用时间来换取安全
加锁的表现:线程对临界区的代码串行执行
加锁的原则:尽量的要保证临界区的代码越少越好
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
在纯互斥环境下,如果锁分配的不合理,容易导致其他线程的饥饿问题
–但是不是说只要有互斥就一定有饥饿
锁本身就是共享资源
申请和释放锁本身就被设计成了原子性操作(申请和释放锁之间那个区域叫做临界区)
当前线程访问临界区的过程,对于其他线程是原子的
–也就是说对于其他线程而言,一个线程要么没锁,要么已经释放锁了
线程在临界区中是可以被切换的–但是锁还是这个线程的
锁的原理:(下面是伪代码)
注意:一条汇编语句就是原子的
lock:就是把0给al寄存器,然后把mutex的值给寄存器 …注意:线程里面存的硬件的上下文(比如:寄存器里面的值),是线程私有的
问题:如果单独给某一个线程不加锁会咋样:容易出现奇怪的问题
加锁相关的接口

pthread_mutex_destroy和pthread_mutex_init的返回值:成功返回0,失败返回非0的错误码
创建和初始化锁的方法:(锁叫lock的话)
pthread_mutex_t lock;
pthread_mutex_init (&lock,nullptr);
释放锁的方法:
pthread_mutex_destroy(&lock);
还有种自动定义初始化和释放锁的方法:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
这个会在自己生命周期开始和结束时自动定义初始化和释放

这三个的返回值:成功返回0,失败返回错误码
pthread_mutex_lock申请锁成功,才能往后执行;不成功的话会阻塞等待
pthread_mutex_trylock的话是不会的
申请锁:
pthread_mutex_lock(&lock);
解开锁:
pthread_mutex_unlock(&lock);
用法:
while (true)
{
pthread_mutex_lock(&lock);
if(tickets > 0)
{
usleep(1000);
printf("who=%s, get a ticket: %d\n", name, tickets);
tickets--;
pthread_mutex_unlock(&lock);
}
else
pthread_mutex_unlock(&lock);
break;
}
锁的封装
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_;
};
封装了之后上面就可以改写成:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
while (true)
{
{
LockGuard lockguard(&lock);
if (tickets > 0)
{
usleep(1000);
tickets--;
}
else
break;
}
这个lockguard就属于RAII风格的了,生命周期结束可以自动解锁
可重入和线程安全的比较
可重入这个针对的是函数的性质
线程安全针对的是多线程并发的问题
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
线程安全:指在多线程编程中,多个线程对临界资源进行争抢访问而不会造成数据二义或程序逻辑混乱的情况
线程安全的实现是通过同步和互斥实现的
引申:STL里面的容器需要使用者去自行维护线程安全
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资
源而处于的一种永久等待状态
死锁的四个必要条件
互斥条件:一个资源每次只能被一个执行流使用 --前提
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放 --原则
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺 --原则
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系 --重要条件
一个锁也是可以形成死锁的:
比如:连续申请同一把锁两次
解决死锁问题的理念:
理念:破坏上面四个必要条件的任意一个就行了
同步
让所有的线程获取锁时按照一定的顺序–这样按照一定顺序性获取资源就叫做同步
实现线程同步机制的话,就要用到条件变量
条件变量的相关接口

pthread_cond_destroy:销毁POSIX 线程中的条件变量形参:
cond:要初始化的条件变量
pthread_cond_init:初始化 POSIX 线程中的条件变量形参:
attr一般填NULL这俩个的返回值:成功返回
0,失败返回对应的错误码
还要自动定义初始化和销毁条件变量的方法:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER跟前面的自动定义初始化和销毁锁的使用方法一样

pthread_cond_wait作用:让当前线程释放互斥锁,当前进程进入等待状态;当被通知唤醒后,自动重新获取互斥锁,继续执行成功:返回
0失败:返回错误码

pthread_cond_signal:唤醒等待cond这个条件变量的一个线程(默认是队列里的第一个线程)
pthread_cond_broadcast:唤醒所有等待在指定条件变量(cond)上的线程
条件变量一定要搭配锁一起使用–因为唤醒线程需要修改共享数据
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)