std::mutex
一般不推荐直接像下面这用使用std::mutex
。需要自己手动上锁和解锁。
void use_lock()
{
while (true)
{
mtx.lock();
shared_data++;
std::cout << "shared_data = " << shared_data << std::endl;
std::cout << "current thread is " << std::this_thread::get_id() << std::endl;
mtx.unlock();
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
void test_lock()
{
std::thread t1(use_lock);
std::thread t2([]() {
while (true)
{
mtx.lock();
shared_data++;
std::cout << "shared_data = " << shared_data << std::endl;
std::cout << "current thread is " << std::this_thread::get_id() << std::endl;
mtx.unlock();
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
});
t1.join();
t2.join();
}
lock_guard
使用类模板std::lock_guard<>
,实现了RAII手法:创建对象构造时自动上锁,析构时自动解锁。
void use_lock()
{
while (true)
{
{
std::lock_guard<std::mutex> lock(mtx);
shared_data++;
std::cout << "shared_data = " << shared_data << std::endl;
std::cout << "current thread is " << std::this_thread::get_id() << std::endl;
} // 这个作用域使得sleep时已经释放锁
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
不得向锁所在的作用域之外传递指针和引用,指向受保护的共享数据。
线程安全的栈
有时候我们可以将对共享数据的访问和修改聚合到一个函数,在函数内加锁保证数据的安全性。但是对于读取类型的操作,即使读取函数是线程安全的,但是返回值抛给外边使用,存在不安全性。比如一个栈对象,在某线程调用empty()
或size()
时,返回值可能是正确的。然而,函数一但返回,其它线程立马就可以访问栈容器,比如push或pop一个元素,刚才调用empty()
或size()
返回的结果就没意义了。
下面是一个线程不安全的stack。两个线程先后交替访问栈,都发现不为空都进行pop,pop两次发生崩溃。
template<typename T>
class threadunsafe_stack
{
private:
std::stack<T> data;
mutable std::mutex m; // 保证可以对一个const对象加锁
public:
threadunsafe_stack() {}
threadunsafe_stack(const threadunsafe_stack& other)
{
std::lock_guard<std::mutex> lock(other.m);
m = other.m;
}
threadunsafe_stack& operator=(const threadunsafe_stack&) = delete;
void push(T t)
{
std::lock_guard<std::mutex> lock(m);
data.push(t);
}
// 问题代码
T pop()
{
std::lock_guard<std::mutex> lock(m);
auto element = data.top();
data.pop();
return element;
}
bool empty() const
{
std::lock_guard<std::mutex> lock(m);
return data.empty();
}
};
当进行如下调用时,就会发生对空栈进行pop的错误。
void test_threadunsafe_unstack()
{
threadunsafe_stack<int> stk;
stk.push(1);
std::thread t1([&stk]() {
if(!stk.empty())
{
std::this_thread::sleep_for(std::chrono::seconds(1)); // 让现象更明显
stk.pop();
}
});
std::thread t2([&stk]() {
if(!stk.empty())
{
std::this_thread::sleep_for(std::chrono::seconds(1)); // 让现象更明显
stk.pop();
}
});
t1.join();
t2.join();
}
可以使用抛出异常的方式,在pop函数中调用一次data.empty()
判断栈是否为空,如果空则抛出异常,在外层使用的时候就需要捕获异常。
但是这个pop函数仍然存在问题,比如T
是一个vector<int>
类型,那么在pop
函数内部element
就是vector<int>
类型,开始element
存储了一些int值,程序没问题,函数执行pop操作, 假设此时程序内存暴增,导致当程序使用的内存足够大时,可用的有效空间不够, 函数返回element
时,就会就会存在vector
做拷贝赋值时造成失败。即使我们捕获异常,释放部分空间但也会导致栈元素已经出栈,数据丢失了。这其实是内存管理不当造成的,但是C++ 并发编程一书中给出了优化方案。
// 实现两个版本的pop
std::shared_ptr<T> pop() // 返回一个shared_ptr指向栈顶弹出的元素
{
std::lock_guard<std::mutex> lock(m);
if (data.empty()) throw empty_stack();
std::shared_ptr<T> res = std::make_shared<T>(data.top());
data.pop();
return res;
}
void pop(T& t) // 传入引用
{
std::lock_guard<std::mutex> lock(m);
if (data.empty()) throw empty_stack();
t = data.top();
data.pop();
}
第一个pop函数也可以不抛出异常,如果栈为空直接返回nullptr
。
std::shared_ptr<T> pop()
{
std::lock_guard<std::mutex> lock(m);
if (data.empty()) return nullptr;
std::shared_ptr<T> res = std::make_shared<T>(data.top());
data.pop();
return res;
}
死锁
加锁顺序相反导致互相等待。避免死锁的一个方式就是将加锁和解锁的功能封装成独立的函数。这样能保证在独立的函数中执行完操作就解锁,不会导致一个函数里使用多个锁的情况。(这个代码比较好写,不贴了)。
同时加锁
但是我们无法避免在一个函数内部使用两个互斥量,我们可以采取同时加锁的方式。
比如当我们对两个对象执行交换操作的函数时,为了保证互换正确完成,我们在函数内需要给这两个对象进行上锁,如果两个线程都运行这个函数,而且调用的参数相反,就可能会造成死锁。可以采用同时加锁的方式解决这个问题。
std::lock + std::lock_guard<>
class some_big_object
{
private:
int data;
public:
friend void swap(some_big_object& lhs, some_big_object& rhs)
{
// ...
}
};
class X // 这是一个结构,包含了一个复杂的成员对象和一个互斥量
{
private:
std::mutex m;
some_big_object some_detail;
public:
X(const some_big_object& sd): some_detail(sd) {}
friend void swap(X& lhs, X& rhs)
{
if(&lhs == &rhs)
return ;
std::lock(lhs.m, rhs.m);
std::lock_guard<std::mutex> lock1(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock2(rhs.m, std::adopt_lock);
swap(lhs.some_detail, rhs.some_detail);
}
};
- 要先判断交换的两个对象是否是同一个对象,如果是则要立即返回,因为对同一个
std:mutex
对象连续上锁两次是未定义行为。 std:lock
在其内部对lhs.m
或rhs.m
加锁时可能就会抛出异常,如果std::lock
在其中一个互斥上成功获取了锁,但在另一个互斥上获取锁时抛出异常,那么第一个锁就会被释放。即“全员共同成败”:或全部锁定成功,或没获取任何锁并抛出异常。std::lock_guard
的构造参数中std::adopt_lock
表明这个互斥量已经被锁上,std::lock_guard
对象构造时不需要在构造函数中再次加锁。(领养锁)
C++17 std::scoped_lock<>
C++17中的std::scoped_lock<>
能对多个互斥量同时加锁,并且自动释放。(RAII)
friend void swap(X& lhs, X& rhs)
{
if(&lhs == &rhs)
return ;
std::scoped_lock guard(lhs.m, rhs.m); // 还使用了C++17的类模板参数推导
swap(lhs.some_detail, rhs.some_detail);
}