线程间共享数据的问题

多线程之间共享数据,最大的问题便是数据竞争导致的异常问题。多个线程操作同一块资源,如果不做任何限制,那么一定会发生错误。例如:

 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++中最基本的互斥量。该类定义在头文件中。

构造函数

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         else16         {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 lock136260 no get lock136260 get lockResult = 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 lock11 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 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 lock_a(lhs._mutex, std::adopt_lock);  //不在上锁3 std::lock_guard 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 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) mutable31     {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(),弹出栈顶元素。毫无疑问,这正是典型的条件竞争。它的根本原因在于函数接口,即使在内部使用互斥保护栈容器中的元素,也无法防范。

消除返回值导致的条件竞争的方法方法一:传入引用接收数据

templateclass myStack{public:    myStack();    ~myStack();    void pop(T& data);        //传入引用接收数据};int main(){    myStack 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 lock_a(lhs._mutex); 9         lock_guard 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 lock_a(lhs._mutex, std::adopt_lock);  //已经上锁,不再加锁11         std::lock_guard 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 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已经被锁,构造函数不在调用lockunique_lock(_Mutex&, adopt_lock_t);    //构造,但不对_Mtx上锁,需后续手动调用unique_lock(_Mutex&, defer_lock_t)//构造,尝试获取锁,不会造成阻塞unique_lock(_Mutex&, try_to_lock_t)//构造 + try_lock_shared_forunique_lock(_Mutex&, const chrono::duration&);//构造 + try_lock_shared_untilunique_lock(_Mutex&, const chrono::time_point&);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&);//尝试锁定关联可定时锁定 (TimedLockable) 互斥,若抵达指定时间点互斥仍不可用则返回bool try_lock_until(const chrono::time_point&);//与另一 std::unique_lock 交换状态void swap(unique_lock& _Other);//将关联互斥解关联而不解锁它 _Mutex* release();//测试是否占有其关联互斥bool owns_lock();//同owns_lockoperator 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 get_lock() 8 { 9     std::unique_lock lock(_Mtx);10     PrepareData();11     return lock;12 }13 14 void ProcessData()15 {16     std::unique_lock lock(get_lock());17     DoSomething();18 }

按适合的粒度加锁

“锁粒度”该术语描述一个锁所保护的数据量。粒度精细的锁保护少量数据,而粒度粗大的锁保护大量数据。锁操作有两个要点:一是选择足够粗大的锁粒度,确保目标数据都受到保护;二是限制范围,务求只在必要的操作过程中持锁。只要条件允许,我们仅仅在访问共享数据期间才锁住互斥,让数据处理尽可能不用锁保护。持锁期间应避免任何耗时的操作,如读写文件。这种情况可用std::unique_lock处理:假如代码不再需要访问共享数据,那我们就调用unlock()解锁;若以后需重新访问,则调用lock()加锁。

 1 std::mutex _Mtx; 2 bool GetAndProcessData() 3 { 4     std::unique_lock 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 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 Deleter21     {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
  • 头文件:

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
  • 头文件:

与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&);//尝试锁定互斥,若直至抵达指定时间点互斥不可用则返回bool try_lock_until(const chrono::time_point&)

共享锁

void lock_shared();        //为共享所有权锁定互斥,若互斥不可用则阻塞bool try_lock_shared();    //尝试为共享所有权锁定互斥,若互斥不可用则返回void unlock_shared();      //解锁互斥(共享所有权)//尝试为共享所有权锁定互斥,若互斥在指定的时限时期中不可用则返回bool try_lock_shared_for(const chrono::duration&);//尝试为共享所有权锁定互斥,若直至抵达指定时间点互斥不可用则返回bool try_lock_shared_until(const chrono::time_point&);

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已经被锁,构造函数不在调用lockshared_lock(mutex_type&, adopt_lock_t)//构造 + try_lock_shared_forshared_lock(mutex_type&, const chrono::duration&)//构造 + try_lock_shared_untilshared_lock(mutex_type&, const chrono::time_point&)shared_lock(shared_lock&&);      //移动构造shared_lock& operator=(shared_lock&&);    //移动赋值,会先解锁

成员函数

//锁定关联的互斥void lock();//尝试锁定关联的互斥bool try_lock();//解锁关联的互斥void unlock();//尝试锁定关联的互斥,以指定时长try_lock_for(const chrono::duration&);//尝试锁定关联的互斥,直至指定的时间点bool try_lock_until(const chrono::time_point&);//解除关联 mutex 而不解锁mutex_type* release();//测试锁是否占有其关联的互斥bool owns_lock();//同owns_lockoperator 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 lhs(_Mtx, defer_lock);             8  9         //上共享锁(读操作)10         shared_lock 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::unique_lock,它们便会处理好递归锁的余下细节。

工作中尽量避免使用递归锁,这可能是一种拙劣的设计,换一种方式,可能用普通锁就解决问题了。比如,提取一个新的函数,在外部先加锁,然后递归调用该函数。

Copyright

本文参考至《c++并发编程实战》 第二版,作者:安东尼·威廉姆斯。本人阅读后添加了自己的理解并整理,方便后续查找,可能存在错误,欢迎大家指正,感谢!