恶性条件竞争通常发生于完成对多于一个的数据块的修改。例如对一个双向链表的结点的修改。该节点有两个指针。
避免条件竞争的两种方式:
方式一:确保只有进行修改的线程才能看到不变量被破坏时的中间状态。从其他访问线程的角度来
看,修改不是已经完成了,就是还没开始。
方式二:对数据结构和不变量的设计进行修改,修改完的结构必须能完成一系列不可分
割的变化,也就是保证每个不变量保持稳定的状态,这就是所谓的无锁编程。
不建议直接使用std::mutex,要保证在所有的函数出口调用unlock();C++标准库为互斥量提供了一个RAII语法的模板类 std::lock_guard ,在构造时就能提供已锁的互斥量,并在析构的时候进行解锁。
#include
#include
#include
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
std::scoped_lock guard(some_mutex);
return std::find(some_list.begin(), some_list.end(), value_to_find) != some_list.end();
}
切勿将受保护数据的指针或引用传递到互斥锁作用域之外,无论是函数返回值,还是存储在外部可见内存,亦或是以参数的形式传递到用户提供的函数中去。下面这个例子是将以参数的形式传递到用户提供的函数中。
#include
#include
#include
#include
#include
#include
#include
class some_data
{
int a;
std::string b;
public:
void do_something() {
a += 1;
}
};
//这个类包装有一个数据,对这个数据进行保护
class data_wrapper
{
private:
some_data data;//这个互斥锁是保护data数据的
std::mutex m;
public:
template<typename Function>
void process_data(Function func)
{
std::lock_guard<std::mutex> l(m);
func(data); // 1 传递“保护”数据给用户函数 当保护数据传递了出去,该保护数据失去了保护。
}
};
some_data* unprotected;
void malicious_function(some_data& protected_data)
{
unprotected = &protected_data;
}
data_wrapper x;
void foo()
{
x.process_data(malicious_function); // 2 传递一个恶意函数
unprotected->do_something(); // 3 在无保护的情况下访问保护数据
}
现在有一个问题是:即使使用了互斥量或其他机制保护了共享数据,就不必再为条件竞争所担忧吗?
答案是否定的。考虑双链表的例子,为了能让线程安全地删除一个节点,需要确保防止对这三个节点(待删除的节点及其前后相邻的节点)的并发访问。如果只对指向每个节点的指针进行访问保护,那就和没有使用互斥量一样,条件竞争仍会发生。看这个例子:
template<typename T, typename Container = std::deque<T>>
class stack
{
public:
explicit stack(const Container&);
bool empty() const;
size_t size() const;
T top();
void push(T const&);
void push(T&&);
void pop();
};
即使stack的私有数据都在互斥锁的保护之下且没有将私有数据传递出去,虽然empty()和size()可能在返回时是正确的,但其它的结果是不可靠的;当它们返回后,其他线程就可以自由地访问栈,并且可能push()多个新元素到栈中,也可能pop()一些已在栈中的元素。这样的话,之前从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::stack 的设计人员将这个操作分为两部分:先获取顶部元素(top()),然后从栈中移
除(pop())的原因是:当将这两个操作合成一个的时候,如果我先将元素从栈中移除(这是一个节点,将指针指向修改一下),然后再复制该元素,当拷贝元素失败的时候,要弹出的数据消失了,但它的确从栈中移除了。
要解决上述提到的条件竞争有四种方式:
**方式一:**传入一个引用。
std::vector<int> result;//栈中的一个元素
some_stack.pop(result);
该方法的缺点是:需要临时构造一个节点的实例。构造函数可能需要参数,代码在这个阶段可能提供不了这个参数。还需要支持赋值操作。
方式二: 无异常抛出的拷贝构造函数或移动构造函数
将两个pop和top两个操作合二为一的时候可能出现拷贝元素失败的问题,如果该元素的拷贝构造函数和移动构造函数是无异常抛出就可以。
方式三: 返回元素的指针
这样就可以避免拷贝。
方式四: 1+2或者1+3
下面使用方式1和方式3写一个线程安全的堆栈类定义。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* class empty_stack : std::exception
{
public:
const char* what() const throw() {
return "Empty stack exception: Cannot perform operation on empty stack";
}
};
* 在C++11之前,throw()被用来指定函数不会抛出任何异常,如果函数抛出了异常,则会导致未定义行为。
在C++11中,应该使用noexcept关键字来代替throw()。在这种情况下,const char* what() const throw()声明了what()函数不会抛出异常。
这表示代码在执行what()函数时不会引发异常。这在异常处理中非常重要,因为如果在处理异常时又引发了新的异常,会导致程序终止。
*/
class empty_stack : std::exception
{
public:
const char* what() const noexcept {
return "Empty stack exception: Cannot perform operation on empty stack";
}
};
template<typename T>
class threadsafe_stack
{
private:
std::stack<T> data;
mutable std::mutex m;
public:
threadsafe_stack()
: data(std::stack<T>()) {}
threadsafe_stack(const threadsafe_stack& other)
{
std::lock_guard<std::mutex> lock(other.m);
data = other.data; // 1 在构造函数体中的执行拷贝
}
threadsafe_stack& operator=(const threadsafe_stack&) = delete;
void push(T new_value)
{
std::lock_guard<std::mutex> lock(m);
data.push(new_value);
}
//这两个pop操作的设计可以避免获取栈顶元素不对的问题。
/*
* stack s;
if (! s.empty()){ // 1
int const value = s.top(); // 2
s.pop(); // 3
do_something(value);
}
如果线程1执行完2后,线程3执行了2和3就会导致两个线程获取到了同一个值,但是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();
}
bool empty() const
{
std::lock_guard<std::mutex> lock(m);
return data.empty();
}
};
int main()
{
while (true);
return 0;
}
线程不安全的stack对于pop接口的设计中,如果有下面的情况存在会出现问题。
stack<int> s;
if (! s.empty()){ // 1
int const value = s.top(); // 2
s.pop(); // 3
do_something(value);
}
如果线程1执行完位置2后,线程2执行了位置2和位置3就会导致两个线程获取到了同一个值,但是pop了两次;使用线程安全的stack的接口不会出现获取栈顶元素不对的问题。
对于这句代码的理解:if (data.empty()) throw empty_stack();
旧的stack会导致未定义行为,而这个将会有异常抛出,用户可以自行决定该如何做。
存在两个锁或者两个以上的锁的时候,可能会出现死锁。死锁是两个线程互相等待,从而什么耶做不了。
避免死锁的一般建议,就是让两个互斥量总以相同的顺序上锁。
std::mutex是不可重入的互斥量,不能在同一线程中连续两次锁定。加几次锁必须解几次锁,不能多也不能少
举一个经典的死锁例子:
#include
#include
#include
std::mutex m1;
std::mutex m2;
void threadA()
{
std::unique_lock<std::mutex> lock1(m1);
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 稍作延时,使线程B中的锁得到执行
std::unique_lock<std::mutex> lock2(m2); // 线程A尝试获取锁m2
}
void threadB()
{
std::unique_lock<std::mutex> lock2(m2);
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 稍作延时,使线程A中的锁得到执行
std::unique_lock<std::mutex> lock1(m1); // 线程B尝试获取锁m1
}
int main()
{
std::thread t1(threadA);
std::thread t2(threadB);
t1.join();
t2.join();
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// 这里的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::adopt_lock是一个标志,指示std::lock_guard对象构造函数不需要再次加锁互斥量。
它假设互斥量已在当前线程上被提前锁定,std::lock_guard对象将负责互斥量的解锁
*/
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);
}
};
'if (&lhs == &rhs) return;
'这句代码是必须的。如果没有这句代码,可能导致同一个线程获取两次一个互斥量。
std::lock 要么将两个锁都锁住,要不一个都不锁。
C++17提供了 std::scoped_lock<> 一种新的RAII类型模板类型,与std::lock_guard<> 的功能等价。 std::scoped_lock能接受不定数量的互斥锁类型作为模板参数。
swap函数重写如下:
void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
std::scoped_lock guard(lhs.m,rhs.m); // 1
swap(lhs.some_detail,rhs.some_detail);
}
建议一:避免嵌套锁
一个线程已获得一个锁时,再别去获取第二个。当你需要获取多个锁,使用一
个 std::lock 来做这件事,避免产生死锁。
建议二:避免在持有锁时调用用户提供的代码
建议三:使用固定顺序获取锁
建议四:使用锁的层次结构