在线程间共享数据—互斥量
std::mutex
std::mutex
是C++标准库中 <mutex>
头文件中定义的一个类,它是实现多线程同步的基础工具,主要用于保护共享资源,防止多个线程同时访问同一资源造成的数据竞争(race condition)。
std::mutex
表示一个互斥量(Mutex),它有两个主要的操作:
-
锁定(Locking):通过调用
lock()
函数来获取互斥锁。如果互斥量此时未被任何线程锁定,则当前线程获得该锁,否则线程将阻塞(即挂起),直到互斥量可用。std::mutex m; m.lock(); // 在此处访问或修改受保护的资源 m.unlock();
-
解锁(Unlocking):通过调用
unlock()
函数释放互斥锁,使得其他等待该锁的线程有机会获得锁并继续执行。m.unlock();
另外,std::mutex
还提供了诸如 try_lock()
(尝试非阻塞地获取锁)和 lock_guard
、unique_lock
等辅助工具,帮助更安全、便捷地管理锁的生命周期。
std::lock_guard
std::lock_guard
是C++标准库 <mutex>
中的一个类,用于管理互斥锁(如 std::mutex
)的生命周期,遵循RAII原则。它的主要目的是确保互斥锁在进入特定作用域时自动上锁,并在离开该作用域时自动解锁,从而有效地避免手动管理锁可能导致的问题,如忘记解锁、异常安全问题等。
使用 std::lock_guard
的典型方式如下:
#include <mutex>
std::mutex mtx;
void safe_operation() {
std::lock_guard<std::mutex> guard(mtx); // 当guard创建时自动锁定mtx
// 在此作用域内,互斥锁处于锁定状态,保证了对共享资源的独占访问
// 对共享资源的访问和修改代码...
} // 当guard超出作用域被销毁时,会自动解锁mtx
在上述代码中,当 safe_operation
函数开始执行时,std::lock_guard
构造器会立即锁定 mtx
互斥量。然后,在 safe_operation
函数执行完毕(无论正常结束还是抛出异常)时,std::lock_guard
的析构函数会被自动调用,从而解锁互斥量。这样就确保了互斥锁始终会在适当的时候得到释放,提高了程序的健壮性和安全性。
std::unique_lock
std::unique_lock
是一个类,用于管理互斥量(如 std::mutex
、std::recursive_mutex
等)的锁定和解锁操作。它遵循RAII(Resource Acquisition Is Initialization)原则,确保互斥量在适当的时间被锁定和解锁,即便在处理过程中遇到异常也能保证资源的安全性。
std::unique_lock
的特点和常见用法如下:
- 构造函数:
- 默认构造函数创建一个不关联任何互斥量的
std::unique_lock
对象。 - 接收一个互斥量作为参数的构造函数会立即尝试锁定互斥量。
- 通过传递一个额外的布尔标志
defer_lock
或try_to_lock
到构造函数,可以选择延迟锁定或尝试非阻塞锁定互斥量。
- 默认构造函数创建一个不关联任何互斥量的
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx); // 直接锁定互斥量
std::unique_lock<std::mutex> deferred_lock(mtx, std::defer_lock); // 延迟锁定互斥量
std::unique_lock<std::mutex> try_lock(mtx, std::try_to_lock); // 尝试非阻塞锁定互斥量
-
方法:
lock()
:锁定互斥量,如果互斥量已被锁定,则当前线程将阻塞,直到互斥量可用。unlock()
:解锁互斥量。try_lock()
:尝试非阻塞地锁定互斥量,若成功则返回true
,否则返回false
。owns_lock()
:检查std::unique_lock
是否持有互斥锁。
-
RAII 特性:
- 当
std::unique_lock
对象离开作用域时,如果仍持有互斥锁,则会自动释放(解锁)互斥量,从而避免了手动解锁带来的风险。
- 当
-
迁移所有权:
std::unique_lock
允许你转移对互斥量的锁定所有权,这意味着你可以将一个std::unique_lock
对象的所有权转移到另一个对象,而原对象将不再拥有互斥锁。
举例:
std::mutex mtx;
{
std::unique_lock<std::mutex> lock(mtx);
// 在此作用域内,互斥量被锁定
// 对共享资源的访问和修改...
} // lock离开作用域时,互斥量自动解锁
// 或者使用try_lock和unlock
std::unique_lock<std::mutex> ul(mtx, std::try_to_lock);
if (ul.owns_lock()) {
// 已经成功锁定互斥量,执行操作...
} else {
// 未能锁定互斥量,执行其他操作...
}
ul.unlock(); // 手动解锁
总的来说,std::unique_lock
提供了一种安全、便捷的方式来管理互斥量的锁定和解锁,尤其适合用于那些需要保证资源安全访问的多线程环境。
避免死锁–std::lock
std::lock
是一个函数,用于同时锁定多个互斥量(如 std::mutex
、std::recursive_mutex
等),并且保证所有互斥量都是以一种无死锁的方式进行锁定的。这对于需要同时保护多个资源的场景非常有用。
std::lock
函数在成功返回后,它尝试锁定的所有互斥量都将保持锁定状态。
使用 std::lock
的一般形式如下:
#include <mutex>
#include <vector>
std::mutex mtx1, mtx2, mtx3; // 假设有多个需要同时锁定的互斥量
void safe_multiple_access() {
std::vector<std::unique_lock<std::mutex>> locks;
locks.reserve(3); // 预先分配足够的空间
// 使用std::lock同时锁定多个互斥量
std::lock(mtx1, mtx2, mtx3);
// 将锁定的互斥量放入unique_lock容器中以便后续解锁
locks.emplace_back(mtx1, std::adopt_lock);
locks.emplace_back(mtx2, std::adopt_lock);
locks.emplace_back(mtx3, std::adopt_lock);
// 在此作用域内,所有互斥量都已锁定,可以安全地访问相关资源
// ...
} // 退出作用域后,unique_lock对象会自动解锁互斥量
在上述代码中,std::lock
函数接受一个或多个互斥量作为参数,并尽可能按照最优化的顺序尝试锁定它们,如果无法同时锁定所有互斥量,函数将会一直阻塞直到可以成功锁定为止。这种方法可以避免死锁的发生,因为所有互斥量都会以一种一致的顺序尝试锁定。
需要注意的是,std::lock
并不会直接返回锁住的互斥量,因此通常配合 std::unique_lock
或者其他的 RAII机制来确保在适当的时候解锁互斥量。在上面的例子中,我们使用了 std::unique_lock
并设置了 std::adopt_lock
标志来接手已经由 std::lock
锁定的互斥量。当这些 std::unique_lock
对象离开作用域时,它们会自动解锁对应的互斥量。
单次初始化–std::once_flag
std::once_flag
是C++标准库 <mutex>
中的一个类,用于实现单次初始化(也称为一次初始化或惰性初始化)的线程安全机制。在多线程环境下,有时我们希望某些初始化操作只执行一次,不论有多少线程试图执行它。std::once_flag
与 std::call_once
函数配合使用,可以确保某个特定的初始化代码块在整个程序生命周期中仅被执行一次。
基本使用方式如下:
#include <mutex>
std::once_flag init_flag; // 创建一个once_flag实例
// 初始化函数
void initialize() {
// 这里是初始化逻辑,只会被执行一次
// ...
}
// 在多线程环境中安全调用initialize函数
void thread_safe_init() {
std::call_once(init_flag, initialize); // 如果尚未初始化,则执行initialize函数
}
int main() {
std::thread t1(thread_safe_init);
std::thread t2(thread_safe_init);
t1.join();
t2.join();
// 即使这里有更多线程调用 thread_safe_init,initialize 函数也只会执行一次
return 0;
}
在这个例子中,std::once_flag
实例 init_flag
与 std::call_once
函数搭配,确保了 initialize
函数在整个程序运行期间只执行一次,不管有多少线程调用了 thread_safe_init
函数。这样就能在多线程环境下安全地进行单次初始化操作。
读写锁
- std::shared_mutex
std::shared_mutex
是 C++14 引入的标准库中的一个类,用于实现读写锁(Read-Write Lock)的功能。读写锁允许在多线程环境中更高效地管理对共享资源的访问:
- 当多个线程希望读取共享资源时,它们可以同时获取读取锁(shared lock),也就是多个读线程可以并发访问资源。
- 当一个线程希望修改(写入)共享资源时,它必须获取写入锁(exclusive lock)。在写入锁被持有的情况下,任何其他试图获取读取锁或写入锁的线程都会被阻塞,直到写入锁被释放。
使用 std::shared_mutex
可以确保在任何时刻,最多只有一个线程持有写入锁,但可以有任意数量的线程持有读取锁。这样既能保障数据的一致性,又能充分利用多核处理器的优势,在读取密集型操作中提升并发性能。
- std::shared_lock
std::shared_lock
是C++标准库 <shared_mutex>
中的一个类,用于管理读写锁(std::shared_mutex
或兼容的类型)的共享锁部分。共享锁允许多个线程同时读取(共享访问)受保护的资源,但不允许任何线程在同一时间写入(独占访问)资源。
使用 std::shared_lock
的好处在于它可以确保在多线程环境下,多个读取线程可以同时访问共享资源,而不需要互相等待。当线程不再需要读取资源时,std::shared_lock
会在析构时自动释放共享锁。
#include <shared_mutex>
#include <iostream>
std::shared_mutex mtx;
int shared_data = 0;
void reader() {
std::shared_lock<std::shared_mutex> read_lock(mtx);
// 在此作用域内,可以安全地读取 shared_data
std::cout << "Reader: " << shared_data << '\n';
}
void writer() {
std::unique_lock<std::shared_mutex> write_lock(mtx);
// 在此作用域内,可以安全地修改 shared_data
++shared_data;
std::cout << "Writer: Updated to " << shared_data << '\n';
}
int main() {
std::thread reader_thread1(reader);
std::thread reader_thread2(reader);
// 确保读者线程已经开始读取
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::thread writer_thread(writer);
reader_thread1.join();
reader_thread2.join();
writer_thread.join();
return 0;
}