线程间共享数据的问题
多线程之间共享数据,最大的问题便是数据竞争导致的异常问题。多个线程操作同一块资源,如果不做任何限制,那么一定会发生错误。例如:
1 int g_nResource = 0; 2 void thread_entry() 3 { 4 for (int i = 0; i < 10000000; ++i) 5 g_nResource++; 6 } 7 8 int main() 9 { 10 thread th1(thread_entry); 11 thread th2(thread_entry); 12 th1.join(); 13 th2.join(); 14 cout << g_nResource << endl; 15 return 0; 16 }
输出:
10161838
显然,上面的输出结果存在问题。出现错误的原因可能是:
某一时刻,th1线程获得CPU时间片,将g_nResource从100增加至200后时间片结束,保存上下文并切换至th2线程。th2将g_nResource增加至300,结束时间片,保存上下文并切换回th1线程。此时,还原上下文,g_nResource会还原成之前保存的200的值。
在并发编程中,操作由两个或多个线程负责,它们争先恐后执行各自的操作,而结果取决于它们执行的相对次序,每一种次序都是条件竞争。很多时候,这是良性行为,因为全部可能的结果都可以接受,即便线程变换了相对次序。例如,往容器中添加数据项,不管怎么添加,只要容器的容量够,总能将所有数据项填入,我们只关心是否能全部放入,对于元素的次序并不care。
真正让人烦恼的,是恶性条件竞争。要完成一项操作,需要对共享资源进行修改,当其中一个线程还未完成数据写入时,另一个线程不期而访。恶性条件竞争会产生未定义的行为,并且每次产生的结果都不相同,无形中增加故障排除的难度。
归根结底,多线程共享数据的问题大多数都由线程对数据的修改引发的。如果所有共享数据都是只读数据,就不会有问题。因为,若数据被某个线程读取,无论是否存在其他线程也在读取,该数据都不会受到影响。然而,如果多个线程共享数据,只要一个线程开始改动数据,就会带来很多隐患,产生麻烦。解决办法就是使用互斥对数据进行保护。
1 int g_nResource = 0; 2 std::mutex _mutex; //使用互斥 3 void thread_entry() 4 { 5 _mutex.lock(); //加锁 6 for (int i = 0; i < 10000000; ++i) 7 g_nResource++; 8 _mutex.unlock(); //解锁 9 }
输出:
20000000
用互斥保护共享数据
为了达到我们想要效果,C++11引入了互斥(mutual exclusion)。互斥是一把对资源的锁,线程访问资源时,先锁住与该资源相关的互斥,若其他线程试图再给它加锁,则须等待,直至最初成功加锁的线程把该互斥解锁。这确保了全部线程所见到的共享数据是自洽的(self-consistent),不变量没有被破坏。
在C++中使用互斥
std::mutex
std::mutex是c++中最基本的互斥量。该类定义在<mutex>头文件中。
构造函数
1 mutex(); 2 3 //不支持拷贝构造,也不支持移动构造(有定义拷贝,则无移动) 4 mutex(const mutex&) = delete; 5 mutex& operator=(const mutex&) = delete;
刚初始化的互斥处于unlocked状态。
lock()函数
1 void lock();
用于锁住该互斥量,有如下3中情况:
- 当前没有被锁,则当前线程锁住互斥量,在未调用unlock()函数前,线程拥有该锁。
- 被其他线程锁住,则当前线程被阻塞,一直等待其他线程释放锁。
- 被当前线程锁住,再次加锁会产生异常。
unlock()函数
1 void unlock();
解锁,当前线程释放对互斥量的所有权。在无锁情况下调用unlock()函数,将导致异常。
try_lock()函数
bool try_lock();
尝试锁住互斥量,如果互斥量被其他线程占用,该函数会返回false,并不会阻塞线程。有如下3中情况:
- 当前没有被锁,则当前线程锁住互斥量,并返回true,在未调用unlock函数前,该线程拥有该锁。
- 被其他线程锁住,该函数返回false,线程并不会被阻塞。
- 被当前线程锁住,再次尝试获取锁,返回false。
案例
1 int g_nResource = 0; 2 std::mutex _mutex; 3 void thread_entry() 4 { 5 while (1) 6 { 7 if (_mutex.try_lock()) 8 { 9 cout << this_thread::get_id() << " get lock\n"; 10 for (int i = 0; i < 10000000; ++i) 11 g_nResource++; 12 _mutex.unlock(); 13 return; 14 } 15 else 16 { 17 cout << this_thread::get_id() << " no get lock\n"; 18 this_thread::sleep_for(std::chrono::milliseconds(500)); 19 } 20 } 21 } 22 23 int main() 24 { 25 thread th1(thread_entry); 26 thread th2(thread_entry); 27 th1.join(); 28 th2.join(); 29 cout << "Result = " << g_nResource << endl; 30 }
输出:
131988 get lock 136260 no get lock 136260 get lock Result = 20000000
上面代码有一个缺点,就是需要我们手动调用unlock函数释放锁,这是一个安全隐患,并且,在某些情况下(异常),我们根本没有机会自己手动调用unlock函数。针对上面这种情况,c++引入了lock_guard类。
std::lock_guard
std::lock_guard使用RAII手法,在对象创建时,自动调用lock函数,在对象销毁时,自动调用unlock()函数,从而保证互斥总能被正确解锁。该类的实现很简单,直接贴源码:
1 template <class _Mutex> 2 class _NODISCARD lock_guard { // class with destructor that unlocks a mutex 3 public: 4 using mutex_type = _Mutex; 5 6 explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock 7 _MyMutex.lock(); 8 } 9 10 lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock 11 12 ~lock_guard() noexcept { 13 _MyMutex.unlock(); 14 } 15 16 lock_guard(const lock_guard&) = delete; 17 lock_guard& operator=(const lock_guard&) = delete; 18 19 private: 20 _Mutex& _MyMutex; 21 };
std::lock_guard仅提供了构造函数和析构函数,并未提供其他成员函数。所以,我们只能用该函数来获取锁、释放锁。
案例:
1 int g_nResource = 0; 2 std::mutex _mutex; 3 void thread_entry() 4 { 5 lock_guard<mutex> lock(_mutex); 6 for (int i = 0; i < 10000000; ++i) 7 g_nResource++; 8 }
锁的策略标签
std::lock_guard在构造时,可以传入一个策略标签,用于标识当前锁的状态,目前,有如下几个标签,含义如下:
- std::defer_lock:表示不获取互斥的所有权
- std::try_to_lock:尝试获得互斥的所有权而不阻塞
- std::adopt_lock:假设调用方线程已拥有互斥的所有权
这几个标签可以为 std::lock_guard 、 std::unique_lock 和 std::shared_lock 指定锁定策略。
用法如下:
1 std::lock(lhs._mutex, rhs._mutex); //对lhs、rhs上锁 2 std::lock_guard<mutex> lock_a(lhs._mutex, std::adopt_lock); //不在上锁 3 std::lock_guard<mutex> lock_b(rhs._mutex, std::adopt_lock); //不在上锁
组织和编排代码以保护共享数据
使用互斥并不是万能的,一些情况还是可能会使得共享数据遭受破坏。例如:向调用者返回指针或引用,指向受保护的共享数据,就会危及共享数据安全。或者,在类内部调用其他外部接口,而该接口需要传递受保护对象的引用或者指针。例如:
1 class SomeData 2 { 3 public: 4 void DoSomething() { cout << "do something\n"; } 5 }; 6 7 class Operator 8 { 9 public: 10 void process(std::function<void(SomeData&)> func) 11 { 12 std::lock_guard<mutex> lock(_mutex); 13 func(data); //数据外溢 14 } 15 16 private: 17 SomeData data; 18 mutex _mutex; 19 }; 20 21 void GetDataPtr(SomeData** pPtr, SomeData& data) 22 { 23 *pPtr = &data; 24 } 25 26 int main() 27 { 28 Operator opt; 29 SomeData* pUnprotected = nullptr; 30 auto abk = [pUnprotected](SomeData& data) mutable 31 { 32 pUnprotected = &data; 33 }; 34 opt.process(abk); 35 pUnprotected->DoSomething(); //以无锁形式访问本应该受到保护的数据 36 }
c++并未提供任何方法解决上面问题,归根结底这是我们代码设计的问题,需要牢记:不得向锁所在的作用域之外传递指针和引用,指向受保护的共享数据,无论是通过函数返回值将它们保存到对外可见的内存,还是将它们作为参数传递给使用者提供的函数。
发现接口固有的条件竞争
1 void func() 2 { 3 stack<int> s; 4 if (!s.empty()) 5 { 6 int nValue = s.top(); 7 s.pop(); 8 do_something(nValue); 9 } 10 }
在空栈上调用top()会导致未定义行为,上面的代码已做好数据防备。对单线程而言,它既安全,又符合预期。可是,只要涉及共享,这一连串调用便不再安全。因为,在empty()和top()之间,可能有另一个线程调用pop(),弹出栈顶元素。毫无疑问,这正是典型的条件竞争。它的根本原因在于函数接口,即使在内部使用互斥保护栈容器中的元素,也无法防范。
消除返回值导致的条件竞争的方法
方法一:传入引用接收数据
template<typename T> class myStack { public: myStack(); ~myStack(); void pop(T& data); //传入引用接收数据 }; int main() { myStack<DataRes> s; DataRes result; s.pop(result); }
这在许多情况下行之有效,但还是有明显短处。如果代码要调用pop(),则须先依据栈容器中的元素类型构造一个实例,将其充当接收目标传入函数内。对于某些类型,构建实例的时间代价高昂或耗费资源过多,所以不太实用。并且,该类型必须支持拷贝赋值运算符。
方法二:提供不抛出异常的拷贝构造函数,或不抛出异常的移动构造函数
假设某个接口是按值返回,若它抛出异常,则牵涉异常安全的问题只会在这里出现。那么,只要确保构造函数不会出现异常,该问题就可以解决。解决办法是:让该接口只允许哪些安全的类型返回。
方法三:返回指针,指向待返回元素
返回指针,指向弹出的元素,而不是返回它的值,其优点是指针可以自由地复制,不会抛出异常。可以采用std::shared_ptr托管内存资源。
方法四:结合方法一和方法二,或结合方法一和方法三
将上面几种方法结合起来一起使用。
死锁问题
线程在互斥上争抢锁,有两个线程,都需要同时锁住两个互斥,可它们偏偏都只锁住了一个,都在等待另一把锁,上述情况被称为死锁。
防范死锁的建议是:始终按相同顺序对互斥加锁。
1 class A 2 { 3 public: 4 A(int nValue) : m_nValue(nValue) {} 5 friend void Swap(A& lhs, A& rhs) 6 { 7 if (&lhs == &rhs) return; 8 lock_guard<mutex> lock_a(lhs._mutex); 9 lock_guard<mutex> lock_b(rhs._mutex); 10 std::swap(lhs.m_nValue, rhs.m_nValue); 11 } 12 private: 13 int m_nValue; 14 mutex _mutex; 15 }; 16 17 void func(A& lhs, A& rhs) 18 { 19 Swap(lhs, rhs); 20 } 21 22 int main() 23 { 24 A a1(10); 25 A a2(20); 26 thread th1(func, std::ref(a1), std::ref(a2)); //传入参数顺序不同 27 thread th2(func, std::ref(a2), std::ref(a1)); //传入参数顺序不同 28 th1.join(); 29 th2.join(); 30 }
上述代码存在死锁发生的可能。原因是在调用Swap时,加锁顺序不一致,并且,上述例子出错更加的隐蔽,故障排除更困难。为此,c++提供了std::lock()函数。
std::lock()函数
该函数可以一次锁住两个或者两个以上的互斥量。由于内部算法的特性,它能避免因为多个线程加锁顺序不同导致死锁的问题。用法如下:
1 class A 2 { 3 public: 4 A(int nValue) : m_nValue(nValue) {} 5 6 friend void Swap(A& lhs, A& rhs) 7 { 8 if (&lhs == &rhs) return; 9 std::lock(lhs._mutex, rhs._mutex); 10 std::lock_guard<mutex> lock_a(lhs._mutex, std::adopt_lock); //已经上锁,不再加锁 11 std::lock_guard<mutex> lock_b(rhs._mutex, std::adopt_lock); //已经上锁,不再加锁 12 std::swap(lhs.m_nValue, rhs.m_nValue); 13 } 14 15 private: 16 int m_nValue; 17 mutex _mutex; 18 };
std::scoped_lock类
c++17提供了scoped_lock类,该类的用法和std::lock_guard类相似,也是用于托管互斥量。二者区别在于scoped_lock类可以同时托管多个互斥。例如:
1 scoped_lock<mutex, mutex> lock(lhs._mutex, rhs._mutex);
由于c++17自带类模板参数推导,因此,上面代码可以改写为:
1 scoped_lock lock(lhs._mutex, rhs._mutex);
防范死锁的补充准则
虽然死锁最常见的诱因之一是互斥操作,但即使没有牵涉互斥,也会发生死锁现象。例如:有两个线程,各自关联了std::thread实例,若它们同时在对方的std::thread实例上调用join(),就能制造出死锁现象却不涉及锁操作。如果线程甲正等待线程乙完成某一动作,同时线程乙却在等待线程甲完成某一动作,便会构成简单的循环等待。防范死锁的准则最终可归纳成一个思想:只要另一线程有可能正在等待当前线程,那么当前线程千万不能反过来等待它。
准则1:避免嵌套锁
假如已经持有锁,就不要试图获取第二个锁,若每个线程最多只持有唯一一个锁,那么对锁的操作不会导致死锁。万一确有需要获取多个锁,我们应采用std::lock()函数,借单独的调用动作一次获取全部锁来避免死锁。
准则2:一旦持锁,就须避免调用由用户提供的程序接口
若程序接口由用户自行实现,则我们无从得知它到底会做什么,它可能会随意操作,包括试图获取锁。一旦我们已经持锁,若再调用由用户提供的程序接口,而它恰好也要获取锁,此时就会导致死锁。
准则3:依次从固定顺序获取锁
如果多个锁是绝对必要的,却无法通过std::lock()在一步操作中获取全部的锁,我们只能退而求其次,在每个线程内部都依照固定顺序获取这些锁,并确保所有线程都遵从。
准则4:按层级加锁
依照固定次序加锁可能在实际中并不好执行,那么,我们可以自己构建一个层级锁,根据锁的层级结构来进行加锁。但线程已经获取一个较低层的互斥锁,那么,所有高于该层的互斥锁全部不允许加锁。
运用std::unique_lock类灵活加锁
std::unique_lock类同样可以用来托管互斥量,但它比std::lock_guard类更加灵活,不一定始终占有与之关联的互斥。
构造函数
unique_lock(); unique_lock(_Mutex&); //构造并调用lock上锁 ~unique_lock(); //析构并调用unlock解锁 //构造,_Mtx已经被锁,构造函数不在调用lock unique_lock(_Mutex&, adopt_lock_t); //构造,但不对_Mtx上锁,需后续手动调用 unique_lock(_Mutex&, defer_lock_t) //构造,尝试获取锁,不会造成阻塞 unique_lock(_Mutex&, try_to_lock_t) //构造 + try_lock_shared_for unique_lock(_Mutex&, const chrono::duration<_Rep, _Period>&); //构造 + try_lock_shared_until unique_lock(_Mutex&, const chrono::time_point<_Clock, _Duration>&); unique_lock(unique_lock&& _Other); //移动构造 //若占有则解锁互斥,并取得另一者的所有权 unique_lock& operator=(unique_lock&& _Other); //无拷贝构造 unique_lock(const unique_lock&) = delete; unique_lock& operator=(const unique_lock&) = delete;
构造函数提供了灵活的加锁策略。
成员函数
//锁定关联互斥 void lock(); //解锁关联互斥 void unlock(); //尝试锁定关联互斥,若互斥不可用则返回 bool try_lock(); //试图锁定关联的可定时锁定 (TimedLockable) 互斥,若互斥在给定时长中不可用则返回 bool try_lock_for(const chrono::duration<_Rep, _Period>&); //尝试锁定关联可定时锁定 (TimedLockable) 互斥,若抵达指定时间点互斥仍不可用则返回 bool try_lock_until(const chrono::time_point<_Clock, _Duration>&); //与另一 std::unique_lock 交换状态 void swap(unique_lock& _Other); //将关联互斥解关联而不解锁它 _Mutex* release(); //测试是否占有其关联互斥 bool owns_lock(); //同owns_lock operator bool(); //返回指向关联互斥的指针 _Mutex* mutex();
提供了lock()、unlock()等接口,可以随时解锁或者上锁。
在不同的作用域之间转移互斥归属权
因为std::unique_lock实例不占有与之关联的互斥,所以随着其实例的转移,互斥的归属权可以在多个std::unique_lock实例之间转移。通过移动语义完成,注意区分左值和右值。
转移有一种用途:准许函数锁定互斥,然后把互斥的归属权转移给函数调用者,好让他在同一个锁的保护下执行其他操作。代码如下:
1 std::mutex _Mtx; 2 3 void PrepareData() {} 4 5 void DoSomething() {} 6 7 std::unique_lock<std::mutex> get_lock() 8 { 9 std::unique_lock<std::mutex> lock(_Mtx); 10 PrepareData(); 11 return lock; 12 } 13 14 void ProcessData() 15 { 16 std::unique_lock<std::mutex> lock(get_lock()); 17 DoSomething(); 18 }
按适合的粒度加锁
“锁粒度”该术语描述一个锁所保护的数据量。粒度精细的锁保护少量数据,而粒度粗大的锁保护大量数据。锁操作有两个要点:一是选择足够粗大的锁粒度,确保目标数据都受到保护;二是限制范围,务求只在必要的操作过程中持锁。只要条件允许,我们仅仅在访问共享数据期间才锁住互斥,让数据处理尽可能不用锁保护。持锁期间应避免任何耗时的操作,如读写文件。这种情况可用std::unique_lock处理:假如代码不再需要访问共享数据,那我们就调用unlock()解锁;若以后需重新访问,则调用lock()加锁。
1 std::mutex _Mtx; 2 bool GetAndProcessData() 3 { 4 std::unique_lock<std::mutex> lock(_Mtx); 5 DataResource data = GetData(); 6 lock.unlock(); 7 bool bResult = WirteToFile(data); //非常耗时 8 lock.lock(); 9 SaveResult(bResult); 10 return bResult; 11 }
一般地,若要执行某项操作,那我们应该只在所需的最短时间内持锁。换言之,除非绝对必要,否则不得在持锁期间进行耗时的操作,如等待I/O完成或获取另一个锁(即便我们知道不会死锁)。例如,在比较运算的过程中,每次只锁住一个互斥:
1 class Y 2 { 3 private: 4 int some_detail; 5 mutable std::mutex m; 6 int get_detail() const 7 { 8 std::lock_guard<std::mutex> lock_a(m); 9 return some_detail; 10 } 11 public: 12 Y(int sd):some_detail(sd){} 13 friend bool operator==(Y const& lhs, Y const& rhs) 14 { 15 if(&lhs==&rhs) 16 return true; 17 int const lhs_value=lhs.get_detail(); 18 int const rhs_value=rhs.get_detail(); 19 return lhs_value==rhs_value; ⇽--- ④ 20 } 21 };
为了缩短持锁定的时间,我们一次只持有一个锁。
保护共享数据的其他工具
互斥是保护共享数据的最普遍的方式之一,但它并非唯一方式。
在初始化过程中保护共享数据
假设我们需要某个共享数据,而它创建起来开销不菲。因为创建它可能需要建立数据库连接或分配大量内存,所以等到必要时才真正着手创建。这种方式称为延迟初始化(lazy initialization)。最常见的就是实现懒汉式单例模式,现在,时代变了,实现线程安全的单例模式,不需要使用双重锁了!
std::call_once()函数与std::once_flag
std::call_once()函数可以确保可调用对象仅执行一次,即使是在并发访问下。该函数定义如下:
1 template <class _Fn, class... _Args> 2 void(call_once)(once_flag& _Once, _Fn&& _Fx, _Args&&... _Ax);
- _Once:std::once_flag对象,它确保仅有一个线程能执行函数。
- _Fx:待调用的可调用对象。
- _Ax:传递给可调用对象的参数包。
用std::call_once()函数实现单例:
1 class Singleton 2 { 3 public: 4 static Singleton* Ins() 5 { 6 std::call_once(_flag, []() { 7 _ins = new Singleton; 8 }); 9 return _ins; 10 } 11 12 Singleton(const Singleton&) = delete; 13 Singleton& operator=(const Singleton&) = delete; 14 15 protected: 16 Singleton() { std::cout << "constructor" << std::endl; } 17 ~Singleton() { std::cout << "destructor" << std::endl; } //必须声明为私有,否则返回指针将可析构 18 19 private: 20 struct Deleter 21 { 22 ~Deleter() { 23 delete _ins; 24 _ins = nullptr; 25 } 26 }; 27 static Deleter _deleter; 28 static Singleton* _ins; 29 static std::once_flag _flag; 30 }; 31 32 Singleton::Deleter Singleton::_deleter; 33 Singleton* Singleton::_ins = nullptr; 34 std::once_flag Singleton::_flag;
Deleter确保Singleton对象销毁时,能够释放_ins对象。
Magic Static特性
C++11标准中定义了一个Magic Static特性:如果变量当前处于初始化状态,当发生并发访问时,并发线程将会阻塞,等待初始化结束。
用Magic Static特性实现单例:
1 class Singleton 2 { 3 public: 4 static Singleton& Ins() 5 { 6 static Singleton _ins; 7 return _ins; 8 } 9 10 Singleton(const Singleton&) = delete; 11 Singleton& operator=(const Singleton&) = delete; 12 13 protected: 14 Singleton() { std::cout << "constructor" << std::endl; } 15 ~Singleton() { std::cout << "destructor" << std::endl; } 16 };
保护甚少更新的数据结构
考虑一个存储着DNS条目的缓存表,它将域名解释成对应的IP地址。给定的DNS条目通常在很长时间内都不会变化——在许多情况下,DNS条目保持多年不变。尽管,随着用户访问不同网站,缓存表会不时加入新条目,但在很大程度上,数据在整个生命期内将保持不变。为了判断数据是否有效,必须定期查验缓存表;只要细节有所改动,就需要进行更新。
更新虽然鲜有,但它们还是会发生。另外,如果缓存表被多线程访问,更新过程就需得到妥善保护,以确保各个线程在读取缓存表时,全都见不到失效数据。
如果使用传统的互斥,效率可能不高:当更新缓存表时,阻止其他线程访问数据是理所应到。但很多时候,数据未发生改变,但每个线程读取数据都会导致上锁,即读多写少,std::mutex效率就比较低了。
C++17标准库提供了两种新的互斥:std::shared_mutex和std::shared_timed_mutex。
std::shared_mutex
- 平台:c++17
- 头文件: <shared_mutex>
std::shared_mutex类可用于保护共享数据不被多个线程同时访问。与独占式互斥不同,该类拥有两种访问级别:
- 共享 - 多个线程能共享同一互斥的所有权。
- 独占性 - 仅一个线程能占有互斥。
std::shared_mutex有如下特点:
- 若一个线程已获得独占锁(通过lock、try_lock)则无其他线程能获取该锁(包括共享的)。
- 仅当任何线程均未获取独占性锁时,共享锁才能被多个线程获取(通过lock_shared 、try_lock_shared)。
- 在一个线程内,同一时刻只能获取一个锁(共享或独占性)。
构造函数
shared_mutex(); //构造互斥 ~shared_mutex(); //析构互斥 //无拷贝 shared_mutex(const shared_mutex&) = delete; shared_mutex& operator=(const shared_mutex&) = delete;
独占锁
void lock(); //锁定互斥,若互斥不可用则阻塞 void unlock(); //解锁互斥 void try_lock(); //尝试锁定互斥,若互斥不可用则返回
共享锁
void lock_shared(); //为共享所有权锁定互斥,若互斥不可用则阻塞 bool try_lock_shared(); //尝试为共享所有权锁定互斥,若互斥不可用则返回 void unlock_shared(); //解锁共享所有权互斥
案例
1 std::shared_mutex _Mtx; 2 void func() 3 { 4 _Mtx.lock_shared(); 5 cout << " thread Id = " << this_thread::get_id() << " do something!\n"; 6 _Mtx.unlock_shared(); 7 } 8 9 int main() 10 { 11 _Mtx.lock_shared(); //使用共享锁锁住 12 thread th1(func); 13 thread th2(func); 14 th1.join(); 15 th2.join(); 16 _Mtx.unlock_shared(); 17 }
main函数中使用共享锁锁住,实际并不影响其他线程获取共享锁,如果将main函数中的共享锁换成独占锁,程序将发生死锁。同理,如果将func函数中的共享锁换成独占锁,同样会造成死锁,获取独占锁时,如果当前有其他线程正持有共享锁,那么该线程将阻塞,直到其他线程释放共享锁。
std::shared_timed_mutex
- 平台:c++14
- 头文件: <shared_mutex>
与std::shared_mutex类相似,只是提供了额外的成员函数。
构造函数
shared_timed_mutex(); ~shared_timed_mutex(); shared_timed_mutex(const shared_timed_mutex&) = delete; shared_timed_mutex& operator=(const shared_timed_mutex&) = delete;
独占锁
void lock(); //锁定互斥,若互斥不可用则阻塞 void unlock(); //解锁互斥 bool try_lock(); //尝试锁定互斥,若互斥不可用则返回 //尝试锁定互斥,若互斥在指定的时限时期中不可用则返回 bool try_lock_for(const chrono::duration<_Rep, _Period>&); //尝试锁定互斥,若直至抵达指定时间点互斥不可用则返回 bool try_lock_until(const chrono::time_point<_Clock, _Duration>&)
共享锁
void lock_shared(); //为共享所有权锁定互斥,若互斥不可用则阻塞 bool try_lock_shared(); //尝试为共享所有权锁定互斥,若互斥不可用则返回 void unlock_shared(); //解锁互斥(共享所有权) //尝试为共享所有权锁定互斥,若互斥在指定的时限时期中不可用则返回 bool try_lock_shared_for(const chrono::duration<_Rep, _Period>&); //尝试为共享所有权锁定互斥,若直至抵达指定时间点互斥不可用则返回 bool try_lock_shared_until(const chrono::time_point<_Clock, _Duration>&);
std::shared_lock
std::shared_lock和std::unique_lock类相似,unique_lock用于操作独占锁,其构造函数将调用lock()函数,析构函数将调用unlock()函数。shared_lock用于操作共享锁,其构造函数将调用lock_shared()函数,析构函数将调用unlock_shared()函数。
构造函数
shared_lock(); shared_lock(mutex_type&); //构造并调用lock_shared上锁 ~shared_lock(); //析构并调用unlock_shared解锁 //构造,但不对_Mtx上锁,需后续手动调用 shared_lock(mutex_type&, defer_lock_t) //构造,尝试获取锁,不会造成阻塞 shared_lock(mutex_type&, try_to_lock_t) //构造,_Mtx已经被锁,构造函数不在调用lock shared_lock(mutex_type&, adopt_lock_t) //构造 + try_lock_shared_for shared_lock(mutex_type&, const chrono::duration<_Rep, _Period>&) //构造 + try_lock_shared_until shared_lock(mutex_type&, const chrono::time_point<_Clock, _Duration>&) shared_lock(shared_lock&&); //移动构造 shared_lock& operator=(shared_lock&&); //移动赋值,会先解锁
成员函数
//锁定关联的互斥 void lock(); //尝试锁定关联的互斥 bool try_lock(); //解锁关联的互斥 void unlock(); //尝试锁定关联的互斥,以指定时长 try_lock_for(const chrono::duration<_Rep, _Period>&); //尝试锁定关联的互斥,直至指定的时间点 bool try_lock_until(const chrono::time_point<_Clock, _Duration>&); //解除关联 mutex 而不解锁 mutex_type* release(); //测试锁是否占有其关联的互斥 bool owns_lock(); //同owns_lock operator bool(); //返回指向关联的互斥的指针 mutex_type* mutex(); //与另一 shared_lock 交换数据成员 void swap(shared_lock& _Right)
案例
1 class A 2 { 3 public: 4 A& operator=(const A& other) 5 { 6 //上独占锁(写操作) 7 unique_lock<shared_mutex> lhs(_Mtx, defer_lock); 8 9 //上共享锁(读操作) 10 shared_lock<shared_mutex> rhs(other._Mtx, defer_lock); 11 12 //上锁 13 lock(lhs, rhs); 14 15 to_do_assignment(); //赋值操作 16 return *this; 17 } 18 private: 19 mutable std::shared_mutex _Mtx; 20 };
递归加锁
假如线程已经持有某个std::mutex实例,试图再次对其重新加锁就会出错,将导致未定义行为。但在某些场景中,确有需要让线程在同一互斥上多次重复加锁,而无须解锁。C++标准库为此提供了std::recursive_mutex,其工作方式与std::mutex相似,不同之处是,其允许同一线程对某互斥的同一实例多次加锁。我们必须先释放全部的锁,才可以让另一个线程锁住该互斥。例如,若我们对它调用了3次lock(),就必须调用3次unlock()。只要正确地使用std::lock_guard<std::recursive_mutex>和std::unique_lock<std::recursive_mutex>,它们便会处理好递归锁的余下细节。
工作中尽量避免使用递归锁,这可能是一种拙劣的设计,换一种方式,可能用普通锁就解决问题了。比如,提取一个新的函数,在外部先加锁,然后递归调用该函数。
Copyright
本文参考至《c++并发编程实战》 第二版,作者:安东尼·威廉姆斯。本人阅读后添加了自己的理解并整理,方便后续查找,可能存在错误,欢迎大家指正,感谢!
标签:std,shared,lock,编程,c++,互斥,mutex,程间,线程 From: https://www.cnblogs.com/BroccoliFighter/p/17700519.html