1 线程间共享数据的问题
1.1 条件竞争
条件竞争:在并发编程中:操作由两个或多个线程负责,它们争先让线程执行各自的操作,而结果取决于它们执行的相对次序,这样的情况就是条件竞争。
诱发恶性条件竞争的典型场景是,要完成一项操作,却需要改动两份或多份不同的数据,而它们只能用单独的指令改动,当其中的一份数据完成改动时,别的线程有可能不期而访。并且由于这样的场景出现的时间窗口小,因此一般很难复现场景定位。
1.2 防止恶性条件竞争
有如下方法:
1 采取保护措施包装数据结构,确保中间状态只对执行改动的线程可见。
2 修改设计,由一连串不可拆分的改动完成数据变更,每个改动都维持不变量不被破坏。这通常称为无锁编程,难以正确编写。如果从事这一层面的开发,就要探究内存模型的细节,以及区分每个线程能够看到什么数据集。
3 修改数据结构来当作事务处理。
2 用互斥保护共享数据
访问一个数据结构前,先锁住与数据相关的互斥,访问结束后再解锁互斥。C++线程库保证了,一旦有线程锁住了某个互斥,若其他线程试图再给他加锁,需要等待。
互斥也可能带来某些问题,比如死锁,对数据的过保护和欠保护。
2.1 std::mutex
C++中使用std::mutex的实例来构造互斥。
可以通过成员函数lock()对其加锁,unlock()进行解锁。但是并不推荐直接调用成员函数,原因是这样需要记住在函数以外的每条代码路径都要调用unlock(),包括异常退出的路径。
取而代之,C++便准库提供了模板std::lock_guard<>,针对互斥类融合实现了RAII:在构造时加锁,在析构时解锁,从而保证互斥总被正确解锁。
#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list;
std::mutex some_mutex;
void add_to_list(int new_value) {
std::lock_guard<std::mutex> guard(some_mutex);
some_list.push_back(new_value);
}
bool list_contains(int value_to_find) {
std::lock_guard<std::mutex> guard(some_mutex);
return std::find(some_list.begin(), some_list.end(), value_to_find) != some_list.end();
}
C++17支持了模板参数推导,使得上述实现可以写成如下样式。并且引入了std::scoped_lock,他是增强版的lock_guard
std::lock_guard guard(some_mutex);
std::scoped_guard guard(some_mutex);
2.1.1 lock(),unlock()的实现方式
lock()的实现方式是在循环中反复调用flag.test_and_set(),其中所采用的次序为std::memory_order_acquire
unlock()实质上是服从std::memory_order_release次序的flag.clear()操作。
当第一个线程调用lock()时,标志flag正处于置零状态,test_and_set()的第一次调用会设置标志成立并返回false,表示负责执行的线程已经获取了锁,遂循环结束,互斥随即生效。该线程可修改受其保护的数据而不受干扰。此时标志已设置成立,如果任何其他线程再调用lock(),都会在test_and_set()所在的循环中阻塞。
当持锁线程完成了受保护数据的改动,就调用unlock(),再进一步按照std::memory_order_release次序语义执行flag.clear()。若第二个线程因调用lock()而反复执行flag.test_and_set(),又因该操作采用了std::memory_order_acquire次序语义,故标志flag上的这两项操作形成同步。
2.2 指针和引用打破互斥保护
如果成员函数返回指针或引用,指向受保护的数据,那么即便成员函数全部按良好、有序的方式锁定互斥,仍然会无济于事。
只要存在任何能访问该指针和引用的代码,它就可以访问受保护的共享数据,而无需锁定互斥。因此,利用互斥保护共享数据,需要谨慎设计程序接口,从而保证互斥已先行锁定,再对受保护的共享数据进行访问。
2.3 组织和编排代码以保护共享数据
我们除了要防止成员函数向调用者传出指针或者引用,还要注意成员函数内部调用的别的函数,也不要向这些函数传递指针或者引用。
#include <mutex>
#include <string>
class some_data {
int a;
std::string b;
public:
void 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();
}
比如上述代码,malicious_function方法将被互斥锁保护的data_wrapper中的some_data的引用赋值给外面的unprotected,导致互斥保护被打破,在外面可直接通过unprotected进行操作。
2.4 发现接口固有的条件竞争
#include <deque>
template<typename T, typename Container=std::deque<T>>
class stack {
public:
explicit stack(const Container&);
explicit stack(Container&& = Container());
template <class Alloc> explicit stack(const Alloc&);
template <class Alloc> stack(const Container&, const Alloc&);
template <class Alloc> stack(Container&, const All

最低0.47元/天 解锁文章
1046

被折叠的 条评论
为什么被折叠?



