目录
在多核 CPU 普及的今天,并发编程已经成为开发者必备的技能。本文将用通俗易懂的方式介绍并发编程中的几个核心概念:线程安全、线程池、函数可重入、死锁及原子性,帮助你快速掌握这些重要知识点。
一、原子性:并发的基石
原子性就像我们日常生活中 "不可分割" 的操作。比如用现金付钱时,"一手交钱一手交货" 就是一个原子操作 —— 要么完成交易,要么不完成,不会出现钱给了货没拿到,或者货拿了钱没给的中间状态。
在编程中,原子操作指的是不能被中断的操作。例如:
- 简单的赋值操作
x = 5是原子的- 而
x++这样的操作就不是原子的,因为它包含了 "读取 x 的值→加 1→写回 x" 三个步骤
现代 CPU 提供了原子指令支持,比如 CAS(Compare-And-Swap)操作,它可以在不使用锁的情况下实现原子更新,是很多并发数据结构的基础。
二、线程安全:多线程下的 "和平共处"
线程安全指的是当多个线程同时访问一个共享资源时,无论线程的执行顺序如何,都不会出现数据不一致或其他意外结果。
举个例子:一个在线计数器,多个用户同时点击 "点赞" 按钮。如果计数器实现是线程安全的,无论多少人同时点赞,最终的数字都是正确的;否则可能出现计数错误
实现线程安全的常见方法:
- 使用锁:如 Java 中的
synchronized,C++ 中的std::mutex- 原子操作:使用原子变量,如
std::atomic<int>- 无锁编程:利用 CAS 等原子指令实现
- Thread-local 存储:让每个线程拥有独立的变量副本
三、函数的可重入性:并发环境的 "可靠员工"
可重入函数是指在被多个线程同时调用时,不会产生任何问题的函数。简单说,就是一个函数可以 "被打断后继续执行" 而不出现错误。
可重入函数的特点:
- 不使用全局变量或静态变量
- 不使用共享资源,或对共享资源的访问是线程安全的
- 不调用不可重入的函数
举例说明:
// 不可重入函数,使用了静态变量
int add(int a) {
static int sum = 0;
sum += a;
return sum;
}
// 可重入函数,无静态变量
int add(int a, int b) {
return a + b;
}
可重入函数一定是线程安全的,但线程安全的函数不一定是可重入的。
四、线程池:高效的 "线程工厂"
线程池就像一个有固定员工数量的工厂,提前雇佣好工人(线程),有任务来了就分配给工人,而不是每次有任务都新招聘工人(创建线程)。
为什么需要线程池?
- 创建和销毁线程会消耗系统资源
- 过多的线程会导致 CPU 频繁切换,降低效率
- 线程池可以控制并发数量,防止资源耗尽
线程池的核心组成:
- 一定数量的工作线程
- 一个任务队列
- 线程池管理器(负责任务分配和线程管理)
生活中的例子:餐厅不会在客人来时才临时招聘厨师,而是保持一定数量的厨师随时待命,客人的订单(任务)会被依次处理。
五、死锁:并发世界的 "交通堵塞"
死锁就像十字路口的四辆车,每辆车都在等待前面的车移动,但谁也不让谁,导致所有车都无法前进。
在编程中,死锁指两个或多个线程相互等待对方释放资源而陷入无限期阻塞的状态。
死锁的四个必要条件:
- 互斥:资源只能被一个线程占用
- 持有并等待:线程持有部分资源,同时等待其他资源
- 不可剥夺:资源不能被强制夺走
- 循环等待:线程形成环形等待链
如何避免死锁?
- 按顺序申请资源:所有线程都按固定顺序申请资源
- 定时释放资源:线程申请资源超时后释放已持有资源
- 一次性申请所有资源:避免 "持有并等待"
- 使用 try_lock:尝试获取锁,失败时释放已获得的锁
举例:如果所有线程都约定 "先获取 A 锁,再获取 B 锁",就不会出现 "线程 1 持有 A 等 B,线程 2 持有 B 等 A" 的死锁情况。
总结
并发编程虽然复杂,但理解了这些核心概念后就会清晰很多:
- 原子性是并发操作的基本保证
- 线程安全确保多线程环境下的数据一致性
- 可重入函数是编写可靠并发代码的基础
- 线程池能高效管理线程资源,提升性能
- 死锁是并发编程的 "陷阱",需要通过合理设计来避免
226

被折叠的 条评论
为什么被折叠?



