并发⚓︎
问题导航
什么是多线程中的惊群现象?如何避免?⚓︎
多个线程阻塞等待同一事件,当事件发生时,系统唤醒所有线程。由于资源互斥,最终仅有一个线程成功获取资源,其余线程经历上下文切换后被迫重新休眠。导致大量无意义的 CPU 调度和缓存失效。
避免方式:
- C++ 条件变量:严格使用单播唤醒
notify_one()替代广播唤醒notify_all()。 - 网络 I/O:Linux 内核已修复
accept()惊群;对于 epoll,可使用EPOLLEXCLUSIVE标志,确保内核仅唤醒挂载的一个实例。
在 C++ 线程池实现中,std::packaged_task<return_type()> 配合 std::bind 的核心机制是什么?⚓︎
- 类型推导: 使用 std::result_of
::type 在编译期推导出任意可调用对象的返回值类型,并定义为 return_type。 这使得线程池接口能够接受任意函数、成员函数或函数对象,并正确处理其返回值类型。 - 参数降维绑定: std::bind(f, args...) 将函数与参数提前硬绑定,生成一个不需要入参的闭包对象。这使得线程池的工作线程在执行任务时无需关心参数传递细节,直接调用绑定后的无参函数即可。
- 任务封装: 由于闭包对象执行时无需参数,因此包装它的 std::packaged_task 被显式实例化为无参函数签名
,从而解耦了任务提交与底层工作线程的执行逻辑。
什么是乐观锁和悲观锁?⚓︎
在并发编程和数据库系统中,当多个线程或事务同时尝试修改同一共享资源时,必须引入并发控制机制以保证数据的一致性与完整性。悲观锁(Pessimistic Lock)与乐观锁(Optimistic Lock)是两种核心的并发控制策略。它们的主要区别在于对数据冲突发生概率的假设以及执行加锁操作的时机。
1. 悲观锁 (Pessimistic Lock)⚓︎
悲观锁假设并发访问中发生数据冲突的概率极高。因此,它采取“先加锁,后操作”的保守策略。
1.1 执行机制⚓︎
- 请求锁:线程在读取或修改数据前,主动向系统(操作系统或数据库管理系统)申请排他锁(Exclusive Lock)。
- 阻塞等待:如果该资源已被其他线程锁定,当前线程将被挂起进入阻塞状态,释放 CPU 执行权,直到锁被释放。
- 执行操作:成功获取锁后,线程执行数据的读取、计算和修改操作。在此期间,其他任何试图访问该资源的线程都会被阻塞。
- 释放锁:操作完成并提交数据后,线程释放锁,唤醒等待队列中的其他线程。
1.2 具体实现示例⚓︎
- 数据库层面:关系型数据库(如 MySQL)中的行级锁。使用 SQL 语句
SELECT ... FOR UPDATE可以锁定查询到的数据行,直到当前事务执行COMMIT或ROLLBACK。 - 编程语言层面:Java 中的
synchronized关键字或java.util.concurrent.locks.ReentrantLock类。
1.3 性能影响⚓︎
- 优势:严格保证了数据操作的串行化,彻底消除了并发冲突导致的数据不一致问题。
- 劣势:引入了极高的上下文切换(Context Switch)开销和线程阻塞延迟。在并发量较大的情况下,系统的整体吞吐量会显著下降。此外,不当的锁申请顺序极易引发死锁(Deadlock)。
2. 乐观锁 (Optimistic Lock)⚓︎
乐观锁假设并发访问中发生数据冲突的概率较低。因此,它在数据的读取和计算阶段不加锁,仅在最终将修改写回系统时,才进行冲突检测(Conflict Detection)。
2.1 执行机制⚓︎
- 无锁读取:线程直接读取目标数据,并在本地内存中保存一份数据副本及当前的标识符(如版本号或时间戳)。
- 本地计算:线程基于本地副本进行业务逻辑计算,生成新数据。在此期间,不阻止其他线程对底层资源的读取或修改。
- 冲突检测与写入:在提交修改时,系统比对底层资源的当前标识符与线程读取时保存的标识符是否一致。
- 如果一致:说明在此期间没有其他线程修改过该数据,允许执行写入操作并更新标识符。
- 如果不一致:说明数据已被其他线程篡改,当前写操作被拒绝。
- 重试或放弃:写操作失败后,线程通常会重新读取最新数据并重复上述过程(称为自旋,Spinning),或者直接向调用方抛出异常。
2.2 具体实现示例⚓︎
- 版本号机制 (Version Number):在数据库表中增加一个
version字段。更新时的 SQL 逻辑形如:UPDATE table SET val = new_val, version = version + 1 WHERE id = 1 AND version = old_version只有当底层的version依然等于读取时的old_version时,更新才会成功。 - CAS 算法 (Compare-And-Swap):底层硬件指令级别的支持。包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。仅当 V 的值等于 A 时,才将 V 的值更新为 B。Java 中的
java.util.concurrent.atomic包下的原子类(如AtomicInteger)即基于 CAS 实现。
2.3 性能影响⚓︎
- 优势:消除了线程阻塞和上下文切换的开销,读操作的性能极高,不存在死锁风险。
- 劣势:如果在写冲突密集的场景下使用,会导致大量的线程在冲突检测失败后不断循环重试。这种无意义的 CPU 自旋(CPU Spinning)会极大地消耗 CPU 资源,反而导致性能低于悲观锁。此外,CAS 机制存在 ABA 问题(数据从 A 变为 B 又变回 A,导致 CAS 误判未发生修改)。
3. 应用场景与选择策略⚓︎
在实际的系统架构设计中,选择悲观锁还是乐观锁取决于具体的业务特征:
- 读多写少 (Read-Heavy):如果系统的操作绝大多数是查询,只有极少数是更新,数据冲突的概率很低,应采用乐观锁。这可以最大化并发读取的吞吐量。
- 写多读少 (Write-Heavy) 或 强冲突场景:如果多个线程频繁地更新同一组数据,使用乐观锁会导致灾难性的重试开销。此时必须采用悲观锁,通过排队机制保证执行效率。
- 操作执行时间:如果数据处理的业务逻辑耗时极长,采用乐观锁会导致重试成本极高,通常更倾向于使用悲观锁(或分布式锁)来独占资源;反之,短时间的内存运算更适合乐观锁。