C++ 互斥量详解

张开发
2026/4/22 17:10:51 15 分钟阅读
C++ 互斥量详解
C 互斥量详解一、C 互斥量详解1、互斥量的基本概念2、 C 标准库中的互斥量2.1、 std::mutex2.2、 std::lock_guard (RAII 包装器)2.3、 std::unique_lock (更灵活的 RAII 包装器)2.4、 std::recursive_mutex2.5、 std::timed_mutex2.6、std::recursive_timed_mutex3、使用互斥量的注意事项4、共享互斥量 (std::shared_mutex / std::shared_timed_mutex) - C175、总结二、代码示例1、代码示例二、运行结果一、C 互斥量详解互斥量是多线程编程中用于保护共享数据、防止数据竞争的核心同步机制。其核心思想是互斥同一时刻只允许一个线程访问受保护的共享资源或代码段称为临界区。1、互斥量的基本概念目的解决多线程并发访问共享资源时可能引发的数据竞争问题。数据竞争会导致程序行为未定义结果不可预测。原理当一个线程需要进入临界区操作共享资源时它必须先锁定一个与该资源关联的互斥量。如果互斥量当前未被锁定该线程获得锁并进入临界区。如果互斥量已被其他线程锁定则当前线程会被阻塞直到锁被释放。特性互斥性同一时刻只有一个线程能持有锁。上锁/解锁线程显式或隐式地请求锁和解锁。阻塞当锁不可用时请求锁的线程会等待。2、 C 标准库中的互斥量C11 在mutex头文件中引入了互斥量相关的类。2.1、std::mutex最基本的互斥量类型。成员函数lock(): 尝试锁定互斥量。如果互斥量当前未被锁定则调用线程获得锁并继续执行。如果互斥量已被其他线程锁定则调用线程被阻塞直到锁可用。unlock(): 解锁互斥量。释放锁的所有权。必须在已经获得锁的线程中调用。try_lock(): 尝试锁定互斥量不阻塞线程。如果调用时互斥量未被锁定则获得锁并返回true如果互斥量已被锁定则立即返回false。注意lock()和unlock()必须成对调用。忘记unlock()会导致死锁。lock()和try_lock()可能抛出std::system_error异常例如当互斥量不可恢复时。手动管理锁容易出错如忘记解锁或在异常路径中解锁推荐使用 RAII 包装器。2.2、std::lock_guard(RAII 包装器)用于简化互斥量的锁定和解锁管理遵循 RAII 原则。原理在构造时锁定给定的互斥量在析构时自动解锁。用法std::mutex mtx;voidsafe_function(){std::lock_guardstd::mutexguard(mtx);// 构造时锁定 mtx// ... 操作共享资源 (临界区) ...}// guard 析构时自动解锁 mtx特点自动管理锁的生命周期确保即使发生异常或提前返回锁也会被正确释放避免死锁。作用域绑定在定义lock_guard的代码块内持有锁。不支持手动解锁或再次上锁。如果需要更灵活的控制考虑std::unique_lock。适用场景临界区在整个作用域内都需要保护且不需要在作用域内手动释放锁或传递锁所有权的情况。2.3、std::unique_lock(更灵活的 RAII 包装器)提供比std::lock_guard更灵活的锁管理。特性同样遵循 RAII 原则在析构时自动解锁如果持有锁。可以延迟锁定、提前解锁、尝试锁定、递归锁定、转移锁的所有权。可以与条件变量 (std::condition_variable) 配合使用。成员函数 (部分)lock(): 锁定关联的互斥量如果尚未锁定。unlock(): 解锁关联的互斥量如果已锁定。try_lock(): 尝试锁定关联的互斥量不阻塞。try_lock_for(duration): 尝试在指定时间段内锁定仅当关联的互斥量是std::timed_mutex时。try_lock_until(time_point): 尝试在指定时间点前锁定仅当关联的互斥量是std::timed_mutex时。owns_lock(): 检查是否持有锁。mutex(): 获取关联的互斥量指针。构造方式std::mutex mtx;// 1. 默认构造不关联互斥量std::unique_lockstd::mutexlock1;// 2. 构造时立即锁定 (类似 lock_guard)std::unique_lockstd::mutexlock2(mtx);// 3. 延迟构造 (defer_lock)构造时不锁定std::unique_lockstd::mutexlock3(mtx,std::defer_lock);lock3.lock();// 稍后手动锁定// 4. 尝试构造 (try_to_lock)构造时尝试锁定但不阻塞std::unique_lockstd::mutexlock4(mtx,std::try_to_lock);if(lock4.owns_lock()){/* 锁定成功 */}// 5. 构造时采用超时策略 (adopt_lock_t 用于已锁定的互斥量)适用场景需要手动控制锁的时机、需要配合条件变量、需要转移锁的所有权、需要尝试锁定或带超时的锁定。2.4、std::recursive_mutex可重入互斥量。允许同一个线程多次锁定同一个互斥量。为什么需要如果一个函数在持有锁的情况下调用了另一个也需要锁定同一个互斥量的函数可能因为递归或间接调用使用std::mutex会导致该线程在第二次调用lock()时死锁因为它在等待自己释放锁。原理内部维护一个锁计数器和持有锁的线程 ID。同一线程每次lock()增加计数每次unlock()减少计数。只有当计数器归零时锁才真正被释放其他线程才能获得它。成员函数同std::mutex(lock(),unlock(),try_lock())。注意解锁次数必须等于锁定次数。性能可能略低于std::mutex。使用需谨慎过度使用递归锁可能掩盖设计问题如过大的临界区或函数调用关系过于复杂。优先考虑重构代码以避免嵌套锁定。2.5、std::timed_mutex带超时功能的互斥量。扩展了std::mutex增加了尝试在指定时间内锁定。额外成员函数try_lock_for(rel_time): 尝试在相对时间段rel_time(如std::chrono::seconds(5)) 内锁定互斥量。成功返回true超时返回false。try_lock_until(abs_time): 尝试在绝对时间点abs_time(如std::chrono::system_clock::now() std::chrono::seconds(5)) 前锁定互斥量。成功返回true超时返回false。用途避免无限期阻塞实现带超时的等待有助于死锁预防和响应性系统设计。2.6、std::recursive_timed_mutex结合了std::recursive_mutex和std::timed_mutex的特性可重入且支持超时锁定。3、使用互斥量的注意事项锁的粒度锁保护的范围临界区应尽可能小。只锁住真正需要共享的数据或操作。过大的临界区会严重降低并发性能。死锁当两个或多个线程相互等待对方释放锁时发生。预防死锁的常见方法按固定顺序上锁所有线程都按相同的全局顺序请求锁。避免嵌套锁尽量避免在持有一个锁时再去请求另一个锁。如果必须确保嵌套顺序一致。使用std::lock或std::try_lock这些函数可以一次性锁定多个互斥量避免因逐个锁定顺序不一致导致的死锁。std::mutex mtx1,mtx2;voidsafe_operation(){std::lock(mtx1,mtx2);// 同时锁定两个互斥量避免死锁风险std::lock_guardstd::mutexlock1(mtx1,std::adopt_lock);// 接管已锁定的锁std::lock_guardstd::mutexlock2(mtx2,std::adopt_lock);// ... 操作共享资源 ...}使用带超时的锁如std::timed_mutex的try_lock_for。性能开销锁定和解锁操作本身有一定开销。频繁的锁竞争会显著降低程序性能。考虑使用更细粒度的锁、无锁数据结构或其他同步机制如原子操作来优化。RAII 优先强烈建议使用std::lock_guard或std::unique_lock来管理锁而不是手动调用lock()和unlock()。这可以确保异常安全避免忘记解锁。避免在持有锁时调用未知代码特别是在持有锁时调用虚函数、回调函数或用户提供的函数。这些函数可能也会尝试获取锁导致死锁或性能问题或者执行耗时操作阻塞其他线程。4、共享互斥量 (std::shared_mutex/std::shared_timed_mutex) - C17C17 引入了读写锁的概念。两种访问模式独占模式 (lock,lock_guardshared_mutex,unique_lockshared_mutex) 也称为写锁。同一时间只能有一个线程持有独占锁。用于修改共享数据。共享模式 (lock_shared,shared_lockshared_mutex) 也称为读锁。同一时间允许多个线程同时持有共享锁。用于读取共享数据。特点适用于读操作远多于写操作的场景可以显著提高并发读取性能。std::shared_lock:RAII 包装器用于在作用域内自动获取和释放共享锁。std::shared_mutex smtx;voidreader(){std::shared_lockstd::shared_mutexlock(smtx);// 获取共享锁// ... 读取共享数据 (多个读者可以同时进行) ...}// 自动释放共享锁voidwriter(){std::unique_lockstd::shared_mutexlock(smtx);// 获取独占锁// ... 修改共享数据 (只有一个写者) ...}// 自动释放独占锁5、总结互斥量是 C 多线程编程中控制并发访问共享资源的基础工具。理解std::mutex及其包装器 (lock_guard,unique_lock) 的使用至关重要。根据具体场景选择合适的互斥量类型基本、递归、超时和锁管理模式RAII。注意锁的粒度、死锁预防和性能影响。对于读多写少的场景C17 的共享互斥量是优化性能的有效手段。始终优先使用 RAII 技术来安全地管理锁的生命周期。二、代码示例1、代码示例#includeiostream#includethread// 线程头文件#includemutex// 互斥量头文件// 共享资源多个线程都会访问intshared_num0;// 定义互斥量std::mutex mtx;// 线程函数对共享数字累加 10000 次voidadd_number(){for(inti0;i10000;i){// 关键部分 // lock_guard 自动加锁离开作用域自动解锁std::lock_guardstd::mutexlock(mtx);// 以下代码同一时间只有一个线程能执行shared_num;// }}intmain(){// 创建两个线程同时操作 shared_numstd::threadt1(add_number);std::threadt2(add_number);// 等待线程执行完毕t1.join();t2.join();// 正确结果应该是 20000std::cout最终结果shared_numstd::endl;return0;}二、运行结果最终结果20000C:\Users\徐鹏\Desktop\新建文件夹\Project3\x64\Debug\Project3.exe(进程10224)已退出代码为0(0x0)。 要在调试停止时自动关闭控制台请启用“工具”-“选项”-“调试”-“调试停止时自动关闭控制台”。 按任意键关闭此窗口...

更多文章