单纯的mutex需要手动进行加锁和解锁,如果加锁解锁不匹配,程序就会出现bug无法运行。此外,因为抛出异常或return等操作可能导致没有解锁就退出的问题。更好的办法是采用”资源分配时初始化”(RAII)方法来加锁、解锁。此时主要会用到lock_guard和unique_lock两个类。
std::lock_guard
- 当构造函数被调用时,该互斥量会被自动锁定
- 当析构函数被调用时,该互斥量会自动解锁
- lock_guard对象不能复制或移动,因此只能在局部作用域中使用
例子如下,两个线程都对一个全局变量进行累加,如果不加锁线程就可能同时对a进行操作,产生矛盾。使用lock_guard,我们不需要手动解锁,在lock_guard被析构时自动解锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include<iostream> #include<mutex> #include<thread>
int a = 0; std::mutex mtx; void func() { for (int i = 0; i < 10000; i++) { std::lock_guard<std::mutex> lg(mtx); a++; } }
int main(){ std::thread t1(func); std::thread t2(func);
t1.join(); t2.join(); std::cout << a << std::endl; return 0; }
|
程序输出:
从lock_guard的源码分析(除了构造函数和析构函数以外,没有其他函数):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| template<class _Mutex> class lock_guard { public: using mutex_type = _Mutex;
explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { _MyMutex.lock(); }
lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) { }
~lock_guard() noexcept { _MyMutex.unlock(); }
lock_guard(const lock_guard&) = delete; lock_guard& operator=(const lock_guard&) = delete;
private: _Mutex& _MyMutex; };
|
explicit: 构造类时不支持隐式转换。也就是必须是一个已被定义为_Mutex类的变量才能用于构造lock_guard
std::unique_lock
相比lock_guard,unique_lock可以对互斥量做更丰富的操作,因此使用更加广泛,但同时占用的资源也会更多。
操作一:自动加锁解锁
用上述例子,将lock_guard换成unique_lock可以实现同样功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int a = 0; std::mutex mtx; void func() { for (int i = 0; i < 10000; i++) { std::unique_lock<std::mutex> lg(mtx); a++; } }
int main(){ std::thread t1(func); std::thread t2(func);
t1.join(); t2.join(); std::cout << a << std::endl; return 0; }
|
操作二:延迟锁定
当调用函数时传入std::defer_lock,构造函数不会自动加锁,而由程序员在后续代码中决定什么时候加锁,加什么样的锁
1
| std::unique_lock<std::mutex> lg(mtx,std::defer_lock);
|
查看源码:
1 2 3 4 5 6 7 8 9 10
| class unique_lock{ xxx
unique_lock(_Mutex& _Mtx, defer_lock_t) noexcept : _Pmtx(_STD addressof(_Mtx)), _Owns(false) { }
xxx }
|
之后可以进行手动加锁:
查看源码:是调用mutex的lock函数
1 2 3 4 5 6
| void lock() { _Validate(); _Pmtx->lock(); _Owns = true; }
|
操作三:尝试加锁并返回是否加锁成功-try_lock
try_lock尝试获得mutex所有权,如果可以就加锁返回true,否则返回false,程序继续往下运行。
如下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> #include <vector> #include <string> #include <thread> #include <mutex> using namespace std;
int a = 0, b = 0; std::mutex mtx;
void func() {
std::unique_lock<std::mutex> lg(mtx, std::defer_lock); if(lg.try_lock()){ a++; std::this_thread::sleep_for(std::chrono::seconds(3)); } else{ b++; } }
int main(){ std::thread t1(func); std::thread t2(func);
t1.join(); t2.join(); std::cout << a << " " << b << std::endl; return 0; }
|
t1获得mtx所有权后,会对a加1,等待2s,此时t2尝试获取mtx所有权,但无法获取,返回false,执行b加1.
也可以写成这样:在构造函数中加入try_to_lock的参数
1 2 3 4 5 6 7 8 9 10 11
| void func() { std::unique_lock<std::mutex> lg(mtx, std::try_to_lock); if(lock.owns_lock()){ a++; std::this_thread::sleep_for(std::chrono::seconds(3)); } else{ b++; } }
|
操作四:在一段时间内尝试加锁-try_lock_for
try_lock_for尝试在一定时间内获得mutex所有权,如果可以就加锁返回true,否则返回false,程序继续往下运行。
如下面的例子:(此时mutex要选用timed_mutex)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> #include <vector> #include <string> #include <thread> #include <mutex> using namespace std;
int a = 0, b = 0; std::timed_mutex mtx;
void func() {
std::unique_lock<timed_mutex> lg(mtx, std::defer_lock); if(lg.try_lock_for(std::chrono::seconds(5))){ std::this_thread::sleep_for(std::chrono::seconds(2)); a++; } else{ b++; } }
int main(){ std::thread t1(func); std::thread t2(func);
t1.join(); t2.join(); std::cout << a << " " << b << std::endl; return 0; }
|
t1获得mtx所有权后,会对a加1,等待2s,此时t2尝试在5s内获取mtx所有权。在等待过程中t1休眠完毕,释放mtx所有权,t2就可以加锁,再对a加1。最终a=2,b=0
特性:所有权可以移动但是不能复制
查看源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| unique_lock(unique_lock&& _Other) noexcept : _Pmtx(_Other._Pmtx), _Owns(_Other._Owns) { _Other._Pmtx = nullptr; _Other._Owns = false; }
unique_lock& operator=(unique_lock&& _Other) { if (this != _STD addressof(_Other)) { if (_Owns) _Pmtx->unlock(); _Pmtx = _Other._Pmtx; _Owns = _Other._Owns; _Other._Pmtx = nullptr; _Other._Owns = false; } return (*this); }
unique_lock(const unique_lock&) = delete; unique_lock& operator=(const unique_lock&) = delete; ```
### call_once()
使用场景:某些类是单例模式,在整个程序中只能创建一个实例,此时多线程调用可能有问题。
如下面的例子,该类使用了一个静态成员函数 getInstance() 来获取单例实例,用了一个静态局部变量 instance 来存储单例实例。由于静态局部变量只会被初始化一次,因此该实现可以确保单例实例只会被创建一次。
```C++
using namespace std;
class Singleton { public: static Singleton& getInstance() { static Singleton instance; return instance; } void setData(int data) { m_data = data; } int getData() const { return m_data; }
private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; int m_data = 0; };
|
但如果多个线程同时调用 getInstance() 函数,可能会导致多个对象被创建,从而违反了单例模式的要求。并且可能会出现多个线程同时调用 setData() 函数来修改m_data,可能会导致数据不一致或不正确的结果。
此时可以用call_once函数,call_once函数将输入一个std::once_flag类型的变量,用于标记该函数是否已经被调用过。将该类改写为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Singleton { public: static Singleton& getInstance() { std::call_once(m_onceFlag, &Singleton::init); return *m_instance; } void setData(int data) { m_data = data; } int getData() const { return m_data; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; static void init() { m_instance.reset(new Singleton); } static std::unique_ptr<Singleton> m_instance; static std::once_flag m_onceFlag; int m_data = 0; };
std::unique_ptr<Singleton> Singleton::m_instance; std::once_flag Singleton::m_onceFlag;
|
此时,我们创建了一个静态成员变量 m_onceFlag 来标记初始化是否已经完成。在 getInstance() 函数中,我们使用 std::call_once 来调用 init() 函数,仅当第一次调用时m_onceFlag有效,可以调用init()。后续再有线程运行到此处无法调用init(),直接返回该实例。
参考资料