多线程下难免会需要资源共享,这样难免会发生异常情况。
#include
#include
class X
{
int* _p;
public:
X(int* p = nullptr) : _p(p) {}
~X() { if(_p) delete _p; }
void read()
{
if(_p) std::cout << "value: " << *_p << std::endl;
}
void reset()
{
if(_p) {
delete _p; // 2
_p = nullptr; // 3
}
}
};
int main()
{
X x(new int(77));
std::thread t1(&X::read, &x), t2(&X::reset, &x);
t1.join();
t2.join();
return 0;
}
编译运行这个例子可以发现有时代码正常运行输出,而有时却发生了段错误。原因在于指针_p被两个线程共享,会有三种情况:
所以需要一种方法使得t1线程访问_p时,t2线程不得对 _p就行操作;同理,t2操作时,t1不得读取。
C++11 提供了互斥量来保护共享数据,思路是线程A在操作共享数据前上锁互斥量,其他线程执行到上锁的互斥量时就会进入阻塞,只有线程A解锁互斥量时, 其他某个线程才能继续上锁,然后那个线程继续执行后续代码。
#include
// 互斥量类,不可移动也不可否则
class std::mutex;
void std::mutex::lock();
// 对互斥量进行上锁, 一次只能有线程成功调用此方法并返回,其他线程执行此方法就会进入阻塞状态, 直到unlock方法调用
void std::mutex::unlock();
// 对lock的互斥量继续解锁,以便其他线程可以解除阻塞而返回
bool std::mutex::try_lock();
// 尝试进行上锁操作。若无其他线程上锁,那么上锁互斥量,返回true;若互斥量也被上锁,那么上锁失败返回false
std::mutex::native_handle_type std::mutex::native_handle();
// 返回互斥量的底层标识符, 可用于扩展,但不建议这样用
#include
class X
{
int* _p;
std::mutex mutex;
public:
X(int* p = nullptr) : _p(p) {}
~X() { if(_p) delete _p; }
void read()
{
mutex.lock();
if(_p) std::cout << "value: " << *_p << std::endl;
mutex.unlock();
}
void reset()
{
mutex.lock();
if(_p) {
delete _p;
_p = nullptr;
}
mutex.unlock();
}
};
修复后的类X,在运行就没段错误了,保证了只有同时只有一个线程来操作_p,其他线程必须等待那个线程操作完后才能继续操作。
需要注意的是,本例子的效率来说多线程比单线程还低,这是因为包含了整个函数,没有代码处于临界区之外,也就是没有一条并行的代码。因此,mutex包含的代码区域越小越好。
每次都要上锁解锁太烦了,C++11,14和17陆续加入了一些智能的锁。
这个锁是在构造函数中帮我们上锁,析构函数解锁,无其他额外的方法,功能简单实用,缺点是不够灵活,想解锁时只能等它销毁才能解锁。
#include
template<typename _Mutex>
class std::lock_guard;
class X
{
int* _p;
std::mutex mutex;
public:
X(int* p = nullptr) : _p(p) {}
~X() { if(_p) delete _p; }
void read()
{
// 小技巧, 使用代码块,避免锁的范围大到整个函数, 出代码块时锁就会销毁
{
std::lock_guard<std::mutex> lock(mutex);
if(_p) std::cout << "value: " << *_p << std::endl;
}
}
void reset()
{
{
std::lock_guard<std::mutex> lock(mutex);
if(_p) {
delete _p;
_p = nullptr;
}
}
}
};
为了解决上个锁的缺点于是诞生了灵活的锁,同样的在构造函数中上锁,但是其提供了一些方法来保证其灵活性,代价就是空间的增大。这个锁是可移动不可复制的。
#include
template<typename _Mutex>
class std::unique_lock;
// 构造函数先不介绍, 因为灵活所以构造函数有点多,这章只需要知道传入互斥量就行了
template<typename _Mutex>
void std::unique_lock::lock();
// 对互斥量上锁
template<typename _Mutex>
void std::unique_lock::unlock();
// 对互斥量解锁
template<typename _Mutex>
void std::unique_lock::try_lock();
// 对互斥量尝试上锁
template<typename _Mutex>
template<typename _Clock, typename _Duration>
bool std::unique_lock::try_lock_until(const chrono::time_point<_Clock, _Duration>& __atime);
// 直到某个时间点就行尝试上锁,时间点到之前上锁成功返回true,否则返回false
template<typename _Mutex>
template<typename _Rep, typename _Period>
bool std::unique_lock::try_lock_for(const chrono::duration<_Rep, _Period>& __rtime);
// 就行一段时间的尝试
// 省去一段无改动的代码
void read()
{
std::unique_lock<std::mutex> lock(mutex);
if(_p) std::cout << "value: " << *_p << std::endl;
}
void reset()
{
std::unique_lock<std::mutex> lock(mutex);
if(_p) {
delete _p;
_p = nullptr;
}
}
引入了线程互斥量后,成功解决了共享资源的问题,但是互斥量不是银弹,带来了新的问题。看下面的代码:
std::mutex mutex_1, mutex_2;
void do_something_1()
{
mutex_1.lock(); // 1
mutex_2.lock(); // 2
// ...
mutex_2.unlock();
mutex_1.unlock();
}
void do_something_2()
{
mutex_2.lock(); // 3
mutex_1.lock(); // 4
// ...
mutex_1.unlock();
mutex_2.unlock();
}
两个任务由两个线程来分担,当线程A执行完1时mutex_1被上锁,线程B这是也执行过了3了mutex_2也被上锁,线程A执行2处代码发现已经上锁只能进入阻塞状态等待解锁,而线程B执行4处代码发现也是上锁的也进入阻塞状态等待解锁,就这样两个线程全在等待解锁,而又无法解锁,这样就称为线程死锁。
过于理论,这里就简单提一下,不是系列的关注点
互斥条件
请求和保持条件
不剥夺条件
环路等待条件
操作系统原理都会谈到,有兴趣可以自己去学下
C++11提供了一种方法,C++17专门提供了防死锁的智能锁来避免这个问题。
#include
template<typename _L1, typename _L2, typename... _L3>
void std::lock(_L1& __l1, _L2& __l2, _L3&... __l3);
// 传入多个互斥量,提供了避免死锁的算法来防止死锁。
// 如果有已经上锁的互斥量,那么先对所有存入的互斥量解锁,然后抛出异常
// 这里需要先介绍下lock_gaurd和unique_lock都有的一个构造函数
lock_guard(mutex_type& __m, adopt_lock_t);
unique_lock(mutex_type& __m, adopt_lock_t);
// 第一个参数都是互斥量,不用介绍了,主要介绍一下第二个参数adopt_lock_t
// 这是一个指定锁定策略,有一下几个常量值
// std::defer_lock 对传入的互斥量不上锁
// std::try_to_lock 尝试对互斥量上锁
// std::adopt_lock 假设互斥量已经上锁
// 这里我也有点搞不明白,哪位大佬详细谈下这几个的区别,感觉都差不多
std::mutex mutex_1, mutex_2;
// lock_guard写法
void do_something_1()
{
std::lock(mutex_1, mutex_2);
std::lock_guard<std::mutex> lock(mutex_1, std::adopt_lock);
std::lock_guard<std::mutex> lock(mutex_2, std::adopt_lock);
// ...
}
// unique_lock写法
void do_something_2()
{
std::unique_lock<std::mutex> lock(mutex_1, std::defer_lock);
std::unique_lock<std::mutex> lock(mutex_2, std::defer_lock);
std::lock(mutex_1, mutex_2);
// ...
}
两种都可以避免死锁的发生,主要看需求选择合适的写法
#include
template<class ...MutexTypes>
class scoped_lock;
// 和lock_gaurd很像,无其他的成员函数,避免死锁的RAll封装
void do_something()
{
// 就一行搞定问题
std::scoped_lock lock(mutex_1, mutex_2);
// ...
}
有时有这样的需求:
这也就是读写锁
#include
// C++17标准
class std::shared_mutex;
// 可以被多个线程共享, 但是只有一个线程可以占有的互斥量
// 有互斥量的几个方法,保证独有性
// 下面几个是共享性的方法
void std::shared_mutex::lock_shared();
bool std::shared_mutex::try_lock_shared();
void std::shared_mutex::unlock_shared();
template<typename _Mutex>
class std::shared_lock;
// 灵活的锁,和unique_lock行为一致
class dns_cache
{
std::map<std::string, dns_entry> entries;
mutable std::shared_mutex mutex;
public:
dns_entry find_entry(const std::string& domain) const
{
// 可以被多个线程读取DNS信息
std::shared_lock<std::shared_mutex> lk(mutex);
const std::map<std::string, dns_entry>::const_iterator it = entries.find(domain);
return (it == entries.end()) ? dns_entry() : it->second;
}
void update_or_add_entry(const std::string& domain, const dns_entry& dns_tetails)
{
// 更新时不可读
std::lock_guard<std::shared_mutex> lk(mutex);
entries[domain] = dns_tetails;
}
};
#include
class std::recursive_mutex;
// 成员方法和mutex一样
// 特点是: 不同线程行为和mutex一致,但是同一线程中能进行多次lock,而mutex不行
// 进行多少次lock,就需要多少次unlock,否则其他线程永远处于阻塞
#include
class std::timed_mutex;
// 除了mutex方法外,又提供了2个定时方法,前面见过很多次了,不说了
template <class _Rep, class _Period>
bool std::timed_mutex::try_lock_for(const chrono::duration<_Rep, _Period>& __rtime);
template <class _Clock, class _Duration>
bool std::timed_mutex::try_lock_until(const chrono::time_point<_Clock, _Duration>& __atime);
#include
class std::recursive_timed_mutex;
// std::recursive_mutex 和 std::timed_mutex 的合体版本
#include
class std::shared_timed_mutex;
// 见这名字应该懂了吧