C++11线程间共享数据
使用全局变量等不考虑安全的方式以及原子变量这里就不进行说明了。
在多线程中的全局变量,就好比现实生活中的公共资源一样,比如你有一个同时只能允许一个人做饭的厨房,那么在你占用期间,你的室友就必须等待。多线程的程序中,如果某个资源同时只允许一个线程占用,这时就出现了竞争条件。
竞争条件
假设你想看电影,影院有很多座位,当然有很多买票的人,假如你和另外一个人同时相中同一场次同一个座位,那这时你俩便存在竞争条件,因为同一场次同一个座位只能卖给一个人。
如果你写多线程程序,那么竞争条件将很容易出现。
避免出现竞争条件
解决竞争条件的方法有很多种,最简单的方法就是在物理上解决,保证每个线程都独享有一份数据相互之间不干扰。当然我们可以使用C++自带的原子变量或者自己实现线程安全数据类型来保证,这种方法准确的来说是伪避免。另外我们可以使用事务的方式确保变量修改唯一,我们可以将多个线程对一个变量的更改记录到log中最终由一个线程汇总提交结果,我们称这种方式为软件事务存储器software transactional memory(STM)
多线程中,保护共享数据最常用的方式还是使用互斥锁
互斥锁
线程间共享数据,首先要了解互斥锁(std::mutex)实现的数据共享,C++11提供了两种基本的锁类型
- std::lock_guard,RAIL风格的互斥锁
- std::unique_lock,RAIL风格的互斥锁,但是控制粒度更细,提供了更加友好的加锁解锁方式
RAII - cppreference.com
空类的作用
在mutex的定义中还提供了三个空数据结构用来控制加锁的方式,很多学习go的明白空结构体的妙用,其实空数据在C++中也有很多地方在巧妙的运用
三个空数据结构定义如下:
/// 使用该类型作为构造函数参数时,不对互斥锁加锁.
struct defer_lock_t { explicit defer_lock_t() = default; };
/// 尝试使用非阻塞的方式对互斥锁加锁
struct try_to_lock_t { explicit try_to_lock_t() = default; };
/// 调用之前确保当前线程已经对互斥锁加锁了,调用之后接管互斥锁
struct adopt_lock_t { explicit adopt_lock_t() = default; };
// 定义对应的const 变量
/// Tag used to prevent a scoped lock from acquiring ownership of a mutex.
_GLIBCXX17_INLINE constexpr defer_lock_t defer_lock { };
/// Tag used to prevent a scoped lock from blocking if a mutex is locked.
_GLIBCXX17_INLINE constexpr try_to_lock_t try_to_lock { };
/// Tag used to make a scoped lock take ownership of a locked mutex.
_GLIBCXX17_INLINE constexpr adopt_lock_t adopt_lock { };
我们可以看下lock_guard的构造函数
// 只有一个入参
explicit lock_guard(mutex_type& __m) : _M_device(__m)
{ _M_device.lock(); }
// 入参有两个参数,其中第二个参数没有实际使用,只是起到按照参数来定位构造函数的作用
lock_guard(mutex_type& __m, adopt_lock_t) noexcept : _M_device(__m)
{ } // calling thread owns mutex
当我们实际加锁时
// 只传一个参数
std::lock_guard<std::mutex> lock(mux);
// 调用的是
explicit lock_guard(mutex_type& __m) : _M_device(__m)
{ _M_device.lock(); }
// 当传入第二个参数时
std::lock_guard<std::mutex> lock(mux, std::adopt_lock);
// 调用的是 这样参数adopt_lock就起到了不加锁的目的
lock_guard(mutex_type& __m, adopt_lock_t) noexcept : _M_device(__m)
{ } // calling thread owns mutex
传入的第二个实参并没有真正使用,只是起到定位函数的作用, 传入不同类型的参数根据函数重载规则就可以调用不同的函数,实现对不同构造函数的调用,使用空类能够将参数传递带来的影响降低到最小。
互斥锁的使用示例
使用全局锁保护全局list的安全
std::list<int> some_list;
std::mutex some_mutex;
// 线程1中调用
void add_to_list(int new_value)
{
some_mutex.lock();
some_list.push_back(new_value);
some_mutex.unlock();
}
// 线程2中调用
bool list_contains(int value_to_find)
{
some_mutex.lock();
auto ret = std::find(some_list.begin(),some_list.end(),value_to_find)
!= some_list.end();
some_mutex.unlock();
return ret;
}
封装多线程安全类,来保证线程间数据安全
class some_data {
int a{};
std::string b;
public:
void do_something();
};
void some_data::do_something() {
}
class data_wrapper {
private:
some_data data;
std::mutex m;
public:
template<typename Function>
void process_data(Function func) {
std::lock_guard<std::mutex> l(m);
func(data);
}
};
some_data *unprotected;
void malicious_function(some_data &protected_data) {
unprotected = &protected_data;
}
data_wrapper x;
void foo() {
x.process_data(malicious_function);
unprotected->do_something();
}
std::lock
C++中求出变参函数变参个数,同样非模板函数也适用
// sizeof...(变参类型) 可求出变参函数变参个数
template<typename L1, typename... L3>
int Data(L1& _l1, L3&... _l3)
{
return (sizeof...(L3));
}
std::lock会尝试对给定的互斥锁进行上锁,没有顺序要求,其中任意一个互斥锁上锁失败都会导致lock阻塞
结合std::lock_guard使用
std::mutex mux1, mux2;
// mux1, mux2,进行加锁,任意一个失败都会导致阻塞
std::lock(mux1, mux2);
// 这里的guard不对互斥锁加锁,只是为了确保函数结束时会自动对互斥锁去锁
std::lock_guard<std::mutex> guard1(mux1, std::adopt_lock);
std::lock_guard<std::mutex> guard2(mux2, std::adopt_lock);
结合std::unique_lock使用
class X {
private:
int data;
std::mutex mux;
public:
friend void swap(X& lhs, X& rhs) {
if (&lhs == &rhs)
return;
// 延迟加锁
std::unique_lock<std::mutex> lock_a(lhs.mux, std::defer_lock);
std::unique_lock<std::mutex> lock_b(rhs.mux, std::defer_lock);
// 在这里调用unique_lock对两个互斥锁进行加锁
std::lock(lock_a, lock_b);
}
};
在结合unique_lock使用时,会更加灵活,因为unique_lock本身实现了lock, unlock,并且支持defer_lock等。
std::lock_guard
std::lock_guard通常用来管理某个锁对象,方便线程对互斥变量的加解锁。在std::lock_guard的生命周期内会保持对某个锁对象的加锁,在std::lock_guard生命周期结束时会对管理的锁对象进行释放。需要注意的是,std::lock_guard并不负责mutex锁对象的生命周期,只是简化了mutex对象加锁和解锁的步骤。
std::unique_lock
std::lock_guard的优点是足够简单,缺点也是简单,使用std::lock_guard程序员不能对锁实现足够灵活的控制。因此std::unique_lock便应运而生,std::unique_lock除了实现std::lock_guard的功能,还增加了对互斥锁更加灵活的控制,程序员可以根据需要在任何地方实现对互斥锁加解锁的人为干预,std::unique_lock具体的实现如下:
// 默认构造函数,unique_lock定义时不对任何锁对象进行管理
unique_lock() noexcept: _M_device(0), _M_owns(false)
// 创建一个unique_lock对象,并对传入的mutex对象加锁
explicit unique_lock(mutex_type& __m)
: _M_device(std::__addressof(__m)), _M_owns(false)
{
lock();
_M_owns = true;
}
// 创建一个unique_lock对象,创建对象时不对mutex对象加锁,并且当前线程没有对mutex对象加锁,
// 在需要的地方需要手动加锁
unique_lock(mutex_type& __m, defer_lock_t) noexcept
: _M_device(std::__addressof(__m)), _M_owns(false)
{ }
// 创建一个unique_lock对象,并尝试对mutex对象进行加锁
unique_lock(mutex_type& __m, try_to_lock_t)
: _M_device(std::__addressof(__m)), _M_owns(_M_device->try_lock())
{ }
// 创建一个unique_lock对象,创建对象时不对mutex对象加锁,并且保证mutex是一个已经被当前线程持有锁的对象
unique_lock(mutex_type& __m, adopt_lock_t) noexcept
: _M_device(std::__addressof(__m)), _M_owns(true)
{
// XXX calling thread owns mutex
}
// 创建一个unique_lock对象,并持有锁到指定的时间点
template<typename _Clock, typename _Duration>
unique_lock(mutex_type& __m, const chrono::time_point<_Clock, _Duration>& __atime)
: _M_device(std::__addressof(__m)),
_M_owns(_M_device->try_lock_until(__atime))
{ }
// 创建unique_lock对象,并持有锁一段时间
template<typename _Rep, typename _Period>
unique_lock(mutex_type& __m,
const chrono::duration<_Rep, _Period>& __rtime)
: _M_device(std::__addressof(__m)),
_M_owns(_M_device->try_lock_for(__rtime))
{ }
// 不允许unique_lock对象赋值和使用一个unique_lock构造另外一个unique_lock对象
unique_lock(const unique_lock&) = delete;
unique_lock& operator=(const unique_lock&) = delete;
// 允许unique_lock对象右值传递
unique_lock(unique_lock&& __u) noexcept
: _M_device(__u._M_device), _M_owns(__u._M_owns)
// 允许unique_lock右值赋值
unique_lock& operator=(unique_lock&& __u) noexcept
// 支持手动加锁
void lock()
// 支持手动尝试加锁
bool try_lock()
// 支持手动尝试加锁到指定的时间点
template<typename _Clock, typename _Duration>
bool try_lock_until(const chrono::time_point<_Clock, _Duration>& __atime)
// 在指定的时间段内尝试加锁,加锁成功返回或者超出时间段返回
template<typename _Rep, typename _Period>
bool try_lock_for(const chrono::duration<_Rep, _Period>& __rtime)
// 支持在需要的地方进行解锁
void unlock()
// 将传入的unique_lock与当前对象进行互换
void swap(unique_lock& __u) noexcept
// 释放对mutex对象的控制,注意这里并不是释放unique_lock对象
mutex_type* release() noexcept
// 用来返回是否对某个锁对象已经加锁
bool owns_lock() const noexcept
// 用来返回是否对某个锁对象已经加锁
explicit operator bool() const noexcept
// 获取unique_lock控制的互斥锁对象
mutex_type* mutex() const noexcept
和lock_guard不同的是,std::unique_lock允许锁之间的传递,比如在两个函数中都需要一把锁中间不间断的一直保护到两个函数结束,那这个时候就可以借用std::unique_lock支持复制传递的特性来实现
std::mutex mux;
std::unique_lock<std::mutex> get_lock() {
std::unique_lock<std::mutex> lock(mux);
// do something
return lock;
}
void process_data() {
std::unique_lock<std::mutex> lock(get_lock());
//do_something();
}
避免死锁的出现
避免持有锁的同时获取另外一把锁
死锁并不是只出现在互斥锁上,当你调用thread.join时也可能出现死锁。比如你在A线程中join B线程在B线程中join A线程,这时A需要等到B结束自己才能结束,B需要等到A结束自己才能结束,就会出现经典的AB锁。想要避免这种死锁也简单,就是不要在一个需要被主线程结束的线程中join其他线程。
如果是锁导致的AB锁,那么就不要获取一个锁之后再获取另外一个,如果你能确保在一个线程中同时只获取一个互斥锁,那么就不可能出现死锁的情况。但是有些情况下我们必须在获取一个锁的同时获取另外一个锁,这个时候就可以借用std::lock来管理互斥锁防止出现死锁的情况,常用方式见std::lock小节。
避免持有锁的同时调用用户提供的代码
用户代码中可能会做任何事情,如果你加锁之后调用用户代码,并且用户再在其代码中调用你其他加锁之后的函数接口,那么就有可能导致死锁的出现。
加锁之后调用用户的代码,那么用户代码中也有可能加锁,并且用户在调用你的代码时有可能也是先加锁再调用的,那么这个时候就有可能出现AB锁的情况。
按照统一的顺序加锁
如果你执行某个步骤时必须进行多次加锁,那么请在任何地方都按照固定顺序进行加锁,如在线程1中先加A锁再加B锁,那么在线程2中也要先加A锁再加B锁。
互斥锁的其他用法
- 使用互斥锁实现线程间共享数据
- 为了避免死锁可以考虑std::lock()或者boost::shared_mutex
- 要尽量保护更少的数据
std::lock用于保证使用多个锁而没有死锁的风险
需要注意的是, 当使
用 std::lock 去锁lhs.m或rhs.m时, 可能会抛出异常; 这种情况下, 异常会传播到 std::lock 之外。 当 std::lock 成功的获取一个互斥量上的锁, 并且当其尝试从另一个互斥
量上再获取锁时, 就会有异常抛出, 第一个锁也会随着异常的产生而自动释放, 所
以 std::lock 要么将两个锁都锁住, 要不一个都不锁
// 这里的std::lock()需要包含<mutex>头文件
class X
{
private:
int some_detail;
std::mutex m;
public:
explicit X(int const& sd):some_detail(sd){}
friend void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
std::lock(lhs.m,rhs.m); // 1
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
// 2
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
// 3
swap(lhs.some_detail,rhs.some_detail);
}
};
#include <mutex>
#include <thread>
struct bank_account {
explicit bank_account(int balance) : balance(balance) {}
int balance;
std::mutex m;
};
void transfer(bank_account &from, bank_account &to, int amount)
{
if(&from == &to) return; // avoid deadlock in case of self transfer
// lock both mutexes without deadlock
std::lock(from.m, to.m);
// make sure both already-locked mutexes are unlocked at the end of scope
std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock);
std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);
// equivalent approach:
// std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
// std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
// std::lock(lock1, lock2);
from.balance -= amount;
to.balance += amount;
}
int main()
{
bank_account my_account(100);
bank_account your_account(50);
std::thread t1(transfer, std::ref(my_account), std::ref(your_account), 10);
std::thread t2(transfer, std::ref(your_account), std::ref(my_account), 5);
t1.join();
t2.join();
}