C++多线程学习---线程间的共享数据

时间:2023-01-31 20:32:06

多线程间的共享数据如果不加以约束是有问题的。最简单的方法就是对数据结构采用某种保护机制,通俗的表达就是:

确保只有进行修改的线程才能看到不变量被破坏时的中间状态。从其他访问线程的角度来看,修改不是已经完成了,就是还没开始。

1.使用互斥量保护共享数据

    当访问共享数据前,使用互斥量将相关数据锁住,再当访问结束后,再将数据解锁。C++标准库为互斥量提供了一个RAII语法的模板类std::lack_guard ,其会在构造的时候提供已锁的互斥量,并在析构的时候进行解锁.

#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list; // 1
std::mutex some_mutex; // 2
void add_to_list(int new_value)
{
std::lock_guard<std::mutex> guard(some_mutex); // 3
some_list.push_back(new_value);
}
bool list_contains(int value_to_find)
{
std::lock_guard<std::mutex> guard(some_mutex); // 4
return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();
}
上述例子通过lock_guart<std::mutex>使得add_to_list和list_contains这两个函数对数据的访问是互斥的。从而保证多线程中数据的安全。


2.堤防接口内在的条件竞争

比如你在写一个栈的数据结构,并且给栈的push\pop\top\empty\size\等接口增加了互斥保护。看以下代码:

stack<int> s;
if (! s.empty()){ // 1
int const value = s.top(); // 2
s.pop(); // 3
do_something(value);
}
以上只是单线程安全,对于多线程在1和2之间,可能有来自另一个线程的pop()调用并删除了最后一个元素,此时就会出问题。

因为锁的粒度太小,需要保护的操作并未全覆盖到。可以考虑适当增大锁的粒度。

std::shared_ptr<T> pop()
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack(); // 在调用pop前,检查栈是否为空
std::shared_ptr<T> const res(std::make_shared<T>(data.top())); // 在修改堆栈前,分配出返回值
data.pop();
return res;
}
void pop(T& value)
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack();
value=data.top();
data.pop();
}

3.堤防死锁问题

造成死锁最大的问题是:由两个或两个以上的互斥量来锁定一个操作。一对线程需要对他们所有的互斥量做一些操作,其中每个线程都有一个互斥量,且等待另一个解锁。这样没有线程能工作,因为他们都在等待对方释放互斥量。

避免死锁的一般建议,就是让两个互斥量总以相同的顺序上锁:总在互斥量B之前锁住互斥量A,可以有效防止大部分问题。一个更好的方法是利用C++提供的std::lock,可以一次性锁住多个互斥量。看以下例子:

class some_big_object;
void swap(some_big_object& lhs,some_big_object& rhs);
class X
{
private:
some_big_object some_detail;
std::mutex m;
public:
X(some_big_object 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);
}
};
调用std::lock() ①锁住两个互斥量,并且两个std:lock_guard 实例已经创建好②③,还有一个互斥量。提供std::adopt_lock 参数除了表示std::lock_guard 对象已经上锁外,还表示现成的锁,而非尝试创建新的锁。

注意:一个互斥量可以在同一线程上多次上锁(std::recursive_mutex)。

以下是设计锁的几条忠告:

1.尽量避免嵌套锁,一个线程已获得锁时别再获取第二个

2.避免在持有锁是调用用户提供的代码,因为用户的代码不可预知,有发生死锁的可能

3.使用固定顺序获取锁--多于多个锁时可有效避免死锁

对于锁的粒度问题也是需要注意的,大的粒度可以保护更多的数据,但是其对性能的影响也越大。同时需要尽可能他将锁的持有时间减到最小。

比较操作符一次锁住一个互斥量:

 
friend bool operator==(Y const& lhs, Y const& rhs){
    if(&lhs==&rhs)        return true;    int const lhs_value=lhs.get_detail(); // 2    int const rhs_value=rhs.get_detail(); // 3    return lhs_value==rhs_value; // 4}


 

 

利用int的拷贝来减少锁的等待时间,是一种高效的做法。