微信导航网站有用吗,昆明做网站建设企业推荐,郑州 网站制作,阿里网站建设方案书1 线程间共享数据的问题 1.1 条件竞争
条件竞争#xff1a;在并发编程中#xff1a;操作由两个或多个线程负责#xff0c;它们争先让线程执行各自的操作#xff0c;而结果取决于它们执行的相对次序#xff0c;这样的情况就是条件竞争。
诱发恶性条件竞争的典型场景是在并发编程中操作由两个或多个线程负责它们争先让线程执行各自的操作而结果取决于它们执行的相对次序这样的情况就是条件竞争。
诱发恶性条件竞争的典型场景是要完成一项操作却需要改动两份或多份不同的数据而它们只能用单独的指令改动当其中的一份数据完成改动时别的线程有可能不期而访。并且由于这样的场景出现的时间窗口小因此一般很难复现场景定位。 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 algorithmstd::listint some_list;
std::mutex some_mutex;
void add_to_list(int new_value) {std::lock_guardstd::mutex guard(some_mutex);some_list.push_back(new_value);
}bool list_contains(int value_to_find) {std::lock_guardstd::mutex guard(some_mutex);return std::find(some_list.begin(), some_list.end(), value_to_find) ! some_list.end();
}
C17支持了模板参数推导使得上述实现可以写成如下样式。并且引入了std::scoped_lock他是增强版的lock_guard
std::lock_guard guard(some_mutex);std::scoped_guard guard(some_mutex); 2.2 指针和引用打破互斥保护 如果成员函数返回指针或引用指向受保护的数据那么即便成员函数全部按良好、有序的方式锁定互斥仍然会无济于事。
只要存在任何能访问该指针和引用的代码它就可以访问受保护的共享数据而无需锁定互斥。因此利用互斥保护共享数据需要谨慎设计程序接口从而保证互斥已先行锁定再对受保护的共享数据进行访问。 2.3 组织和编排代码以保护共享数据 我们除了要防止成员函数向调用者传出指针或者引用还要注意成员函数内部调用的别的函数也不要向这些函数传递指针或者引用。
#include mutex
#include stringclass some_data {int a;std::string b;public:void do_something();
};class data_wrapper {
private:some_data data;std::mutex m;
public:templatetypename Functionvoid process_data(Function func) {std::lock_guardstd::mutex l(m);func(data);}
};some_data* unprotected;void malicious_function(some_data protected_data) {unprotectedprotected_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
templatetypename T, typename Containerstd::dequeT
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 Alloc);template class Alloc stack(stack, const Alloc);bool empty() const;size_t size() const;T top();T const top() const;void push(T const);void push(T);void pop();void swap(stack);template class... Args void emplace(Args... args);
};
上述实现会导致条件竞争也就是empty和size的结果不可信因为在函数返回后其他线程不再受限可能马上会有新元素入栈或者出栈。
线程1线程2if(!s.empty())if(!s.empty()) int const values.top(); int const values.top(); s.pop(); do_something(value); s.pop(); do_something(value);
这样当一个栈只有一个元素的时候第二个pop的线程会导致未定义行为。
并且当我们复制vector时如果vector中的元素数量巨大可能导致因为资源不足造成的内存分配失败。pop函数的定义是返回栈顶元素的值并且将其从栈顶移除。因此只有在栈被改动之后弹出的元素才返回给调用者然而在向调用者复制数据的过程中有可能抛出异常。万一弹出的元素已经从栈上移除但是复制不成功就会造成数据丢失。 2.4.1 消除竞争 2.4.1.1 传入引用
std::vectorint result;
some_stack.pop(result);
优点pop的元素在外部容器白村了生命周期
缺点如果要调用pop还要先闯将一个别的容器。 2.4.1.2 提供不抛出异常的拷贝构造函数或不抛出异常的移动构造函数
这样虽然安全但是效果并不理想。栈容器的用途会受限。 2.4.1.3 返回指针指向弹出元素
优点指针可以自由的复制不会抛出异常。
缺点指向的对象仍然在内存中需要额外的内存管理可以使用shared_ptr。 2.4.1.4 结合12或者13 2.4.1.5 线程安全的栈容器 #include exception
#include memory
#include mutex
#include stackstruct empty_stack: std::exception {const char* what() const throw();
};templatetypename T
class threadsafe_stack {
private:std::stackT data;mutable std::mutex m;
public:threadsafe_stack() {}threadsafe_stack(const threadsafe_stack other) {std::lock_guardstd::mutex lock(other.m);dataother.data;}threadsafe_stack operator(const threadsafe_stack) delete;void push(T new_value) {std::lock_guardstd::mutex lock(m);data.push(std::move(new_value));}std::shared_ptrT pop() {std::lock_guardstd::mutex lock(m);if (data.empty()) throw empty_stack();std::shared_ptrT const res(std::make_sharedT(data.top()));data.pop();return res;}void pop(T value) {std::lock_guardstd::mutex lock(m);if (data.empty()) throw empty_stack();value data.pop();data.pop();}bool empty() const {std::lock_guardstd::mutex lock(m);return data.empty();}
}; 2.5 死锁问题和解决方法 防范死锁的建议通常是始终按照相同的顺序对两个互斥加锁。
C标准提供了std::lock函数使得可以同时锁住多个互斥。
#include mutex
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);std::lock_guardstd::mutex lock_a(lhs.m, std::adopt_lock);std::lock_guardstd::mutex lock_b(rhs.m, std::adopt_lock);swap(lhs.some_detail, rhs.some_detail);}
};
std::adopt_lock对象指明了互斥已被锁住即互斥上有锁存在。std::lock_guard实例据此接收锁的归属权不会在构造函数内试图另行加锁。
无论是正常返回还是异常退出std::lock_guard都保证了互斥全都正确解锁。
另外lock()对lhs.m或rhs.m进行加锁这一函数调用可能导致抛出异常。
C17还提供了全新的特性std::scoped_lock。它和std::lock_guard完全等价。只不过前者是可变参数模板接收各种互斥型别作为模板参数列表还能以多个互斥对象作为构造函数的参数列表。
void swap(X lhs, X rhs)
{if (lhsrhs)return;std::scoped_lock guard(lhs.m, rhs.m);swap(lhs.some_detail, rhs.some_detail);
}
使用新特性实现如上并且上述代码还是用了类模板参数推导C17。使用std::scoped_lock将lock和lock_guard合并为一句降低出错概率。 2.6 防范死锁的补充准则 即使没有牵涉锁也会发生死锁现象。假定有两个线程各自关联了std::thread实例若同时在对方的std::thread实例上调用join那么就能制造出死锁现象。
防范死锁的最终准则只要另一个线程有可能正在等待当前线程那么当前线程不要反过来等待他。 2.6.1 避免嵌套锁
假如已经持有锁就不要试图获取第二个锁。这样保证每个线程最多只持有一个锁仅锁的使用本身不可能导致锁。
但是还存在其他可能引起死锁的场景比如多个线程彼此等待操作多个互斥锁很可能是最常见的死锁诱因。如果真的需要获取多个锁应使用lock函数单独的调用动作一次获取全部锁来避免死锁。 2.6.2 一旦持锁就须避免调用由用户提供的程序接口
若程序接口由用户自行实现则我们无从得知它到底会做什么可能会试图获取锁。这样便可能违反避免嵌套锁的准则可能发生死锁。
不过有时候这个情况难以避免因此在需要调用用户提供的程序接口时要遵守2.6.3准则。 2.6.3 依从固定顺序获取锁
如果多个锁是绝对必要的却无法通过std::lock()在一步操作内全部获取我们只能退而求其次在每个线程内部依从固定顺序获取这些锁。
也可以同时给这些互斥加锁。
或者对于双向链表来说规定遍历的方向让线程总是必须先锁住A再锁住B也可以防范死锁。 2.6.4 按层级加锁
锁的层级划分就是按照特定的方式规定加锁次序在运行期据此查验加锁操作是否遵从预设规则。若某个线程已对低层级互斥加锁则不准它再对高层级互斥加锁。不过这种模式C标准库尚未提供支持自行实现如下
#include limits.h
#include mutexclass hierarchical_mutex {std::mutex internal_mutex;unsigned long const hierarchy_value;unsigned long previous_hierarchy_value;static thread_local unsigned long this_thread_hierarchy_value;void check_for_hierarchy_violation() {if (this_thread_hierarchy_value hierarchy_value) {throw std::logic_error(mutex hierarchy violated);}}void update_hierarchy_value() {previous_hierarchy_value this_thread_hierarchy_value;this_thread_hierarchy_value hierarchy_value;}public:explicit hierarchical_mutex(unsigned long value) :hierarchy_value(value),previous_hierarchy_value(0) {}void lock() {check_for_hierarchy_violation();internal_mutex.lock();update_hierarchy_value();}void unlock() {if (this_thread_hierarchy_value!hierarchy_value) {throw std::logic_error(mutex hierarchy violated);}this_thread_hierarchy_value previous_hierarchy_value;internal_mutex.unlock();}bool try_lock() {check_for_hierarchy_violation();if(!internal_mutex.try_lock()) {return false;}update_hierarchy_value();return true;}
};
thread_local unsigned long hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);hierarchical_mutex high_level_mutex(10000);
hierarchical_mutex low_level_mutex(5000);
hierarchical_mutex other_mutex(6000);int do_low_level_stuff();
int low_level_func() {std::lock_guardhierarchical_mutex lk(low_level_mutex);return do_low_level_stuff();
}int high_level_stuff(int some_param);
int high_level_func() {std::lock_guardhierarchical_mutex lk(low_level_mutex);high_level_stuff(low_level_func());
}void thread_a() {high_level_func();
}int do_other_stuff();void other_stuff() {high_level_func();do_other_stuff();
}void thread_b() {std::lock_guardhierarchical_mutex lk(other_mutex);other_stuff();
} 2.6.5 将准则推广到锁操作之外
如果要等待线程那就值得针对线程规定层级使得每个线程仅等待层级更低的线程。
有一种简单的方法可以实现这种机制让同一个函数启动全部线程且汇合工作也由之负责。 2.7 std::unique_lock
它与std::lock_guard一样也是一个依据互斥作为参数的类模板并且使用RAII手法管理锁。
std::unique_lock放宽了不变量的成立条件因此更灵活一些。std::unique_lock对象不一定始终占有与之关联的互斥。
其构造函数接收两个参数互斥锁和lock实例。
可以传入std::adopt_lock实例借此指明std::unique_lock对象管理互斥上的锁。
也可以传入std::defer_lock实例从而使互斥再完成构造时处于无锁状态等以后有需要时才在std::unique_lock对象不是互斥对象上调用lock()而获取锁或者把std::unique_lock对象交给std::lock()函数加锁。
#include mutex
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::unique_lockstd::mutex lock_a(lhs.m, std::defer_lock);std::unique_lockstd::mutex lock_b(rhs.m, std::defer_lock);std::lock(lock_a, lock_b);swap(lhs.some_detail, rhs.some_detail);}
}; 因为std::unique_lock类具有成员lock()try_lock()unlock()所以它的实例可以传给lock()函数。std::unique_lock底层与目标互斥关联此互斥也有这三个同名函数因此上述函数调用转由它们执行。
std::unique_lock实例还有一个内部标志随着函数的执行而更新表明关联的互斥目前是否正被该类的实例占据。这个标志可以通过owns_lock()查询。
不过最好还是用C17提供的变参模板类std::scoped_lock除非必须用std::unique_lock类进行某些操作如转移锁的归属权。 2.8 在不同作用域之间转移互斥归属权 转移有一种用途准许函数锁定互斥然后把互斥的归属权转移给函数调用者好让他在同一个锁的保护下执行其他操作。
std::unique_lock实例可以在被销毁前解锁这意味着在执行流程的任意分支上若某个锁没必要继续持有则可解锁。这对应用程序的性能来说很重要。 2.9 按适合的粒度加锁 持锁期间应避免任何耗时的操作如I/O操作加锁将毫无必要地阻塞其他线程文件操作通常比内存慢几百上千倍
这种情况可以用std::unique_lock处理假设代码不再需要访问共享数据那就调用unlock()解锁若需要重新访问就调用lock()加锁。 3 保护共享数据的其他工具 3.1 仅在初始化过程中保护共享数据 #include memory
#include mutex
std::shared_ptrsome_resource resource_ptr;
std::mutex resource_mutex;
void foo() {std::unique_lockstd::mutex lk(resource_mutex);if (!resource_ptr) {resource_ptr.reset(new some_resource);}lk.unlock();resource_ptr-do_something();
}
上述代码迫使多个线程循序运行问题较大。为此其中一个备受诟病的改进就是双重检验锁定模式。
3.1.1 双重检验锁定模式 void undefined_behaviour_with_double_checked_locking() {if (!resource_ptr) {std::lock_guardstd::mutex lk(resource_mutex);if (!resource_ptr) {resource_ptr.reset(new some_resource);}}resource_ptr-do_something();
}
这种模式的思路如下
在无锁的条件下读取指针如果为空获取锁。
为了避免在一个线程进入第一个判断后其他线程已经获取锁并且已经为resource_ptr赋值因此在获取锁和赋值之间加入第二个判断保证不会重复初始化让resource_ptr被赋值两次。
但是这种思路可能导致恶性竞争
在第一个判断一个线程想要读取resource_ptr的值的时候另一个线程可能已经获取锁并且正在为resource_ptr执行写操作。但是这个时候可能new some_resource还未生效被无视导致前一个线程虽然能在第一个判断时了解到resource_ptr不为空但是他后面又会走到resource_ptr-do_something()这个时候使用了一个中间态的数据执行do_somthing()。就产生了读写操作的不同步。也就是数据竞争是条件竞争的一种。 3.1.2 std::once_flag和std::call_once函数
用来专门处理上述数据竞争的情况
令所有线程共同调用std::call_once()函数确保在该调用返回时指针初始化由其中某一线程安全且唯一地完成通过合适的同步机制。
必要的同步数据由std::once_flag实例存储每个std::once_flag实例对应一次不同的初始化。
#include memory
#include mutex
std::shared_ptrsome_resource resource_ptr;
std::once_flag resource_flag;
void init_resource() {resource_ptr.reset(new some_resource);
}void foo() {std::call_once(resource_flag, init_resource);resource_ptr-do_something();
}
上述代码中的call_once函数包含两个对象需要初始化数据some_resource和once_flag对象两者的作用域都完整涵盖了它们所属的名字空间。
3.1.2.2 对于类的数据成员的初始化 #include mutexclass X {
private:connection_info connection_details;connection_handle connection;std::once_flag connection_init_flag;void open_connection() {connection connection_manager.open(connection_details);}
public:X(connection_info const connection_details_) :connection_details(connection_details_) {}void send_data(data_packet const data) {std::call_once(connection_init_flag, X::open_connection, this);connection.send_data(data);}data_packet receive_data() {std::call_once(connection_init_flag, X::open_connection, this);return connection.receive_data();}
};
上述代码中的初始化在send或者receive中进行由于这个时候传入call_once的是类成员因此需要传入this指针作为附加参数。
std::once_flag不可复制也不可移动这点与std::mutex类似。 3.1.2.3 使用静态变量代替成员变量进行初始化 class my_class;my_class get_my_class_instance()
{static my_class instance;return instance;
}
把局部变量声明成静态数据在C11之后规定静态数据初始化只会在某一线程单独发生不会出现多个线程都认为自己应当为其赋值的情况在初始化完成前其他线程不会越过静态数据的声明而运行。某些类的代码只需要用到唯一一个全局实例这种情况下可以用静态成员代替std::call_once。来让多个线程可以安全地调用上述方法。 3.2 保护很少更新的数据结构std::shared_mutex和std::shared_timed_mutex C17标准库提供了两种新的互斥std::shared_mutex和std::shared_timed_mutex。
C14标准库只有std::shared_timed_mutex。
C11标准库都没有。可以使用Boost库。
std::shared_mutex相较于std::shared_timed_mutex后者支持更多的操作前者在某些平台上可能会到来额外性能收益。 利用std::shared_mutex实施同步操作
更新操作使用std::lock_guardstd::shared_mutex或者std::unique_lockstd::shared_mutex锁定代替std::mutex.
共享锁std::shared_lockstd::shared_mutex实现共享访问。C14引入工作原理是RAII过程。假设它被某些线程持有要等线程全部释放共享锁其他线程才能访问排他锁。如果任一线程持有排他锁其他线程无法获取共享锁以及排他锁直至排他锁被释放。 #include map
#include string
#include mutex
#include shared_mutexclass dnc_entry;
class dns_cache {std::mapstd::string, dns_entry entries;mutable std::shared_mutex entry_mutex;
public:dnc_entry find_entry(std::string const domain) const {std::shared_lockstd::shared_mutex lk(entry_mutex);std::mapstd::string, dns_entry::const_iterator const it entries.find(domain);return (it entries.end()) ? dns_entry() : it-second;}void update_or_add_entry(std::string const domain, dns_entry const dns_details) {std::lock_guardstd::shared_mutex lk(entry_mutex);entries[domain] dns_details;}
}; 3.3 递归加锁std::recursive_mutex
某些场景需要让线程在同一互斥上多次加锁而无需解锁。为此提供了std::recursive_mutex。
其允许同一线程对某互斥多次加锁必须先释放全部锁才能让另一个线程获取锁。
也是通过std::lock_guardstd::recursive_mutex或者std::unique_lockstd::recursive_mutex 递归互斥常常用于这样的情形
每个公有函数都需要先锁住互斥才进行操作但是当共有函数调用共有函数时使用std::mutex就会有问题因此这个时候使用递归互斥。但是一般不建议这样做因为这可能意味着设计有问题。 4 小结 4.1 几种互斥锁
std::mutexstd::shared_mutex假设它被某些线程持有要等线程全部释放共享锁其他线程才能访问排他锁。如果任一线程持有排他锁其他线程无法获取共享锁以及排他锁直至排他锁被释放性能更优std::shared_timed_mutex假设它被某些线程持有要等线程全部释放共享锁其他线程才能访问排他锁。如果任一线程持有排他锁其他线程无法获取共享锁以及排他锁直至排他锁被释放支持操作更多std::recursive_mutex其允许同一线程对某互斥多次加锁必须先释放全部锁才能让另一个线程获取锁 4.2 几种加锁实例 std::lock_guard依据互斥作为参数的类模板并且使用RAII手法管理锁std::scoped_lock它和std::lock_guard完全等价。只不过前者是可变参数模板接收各种互斥型别作为模板参数列表同时锁住多个std::unique_lock 它与std::lock_guard一样除了std::unique_lock对象不一定始终占有与之关联的互斥 4.3 其他
std::adopt_lock传入std::adopt_lock实例指明std::unique_lock对象管理互斥上的锁。std::defer_lock 传入std::defer_lock实例从而使互斥再完成构造时处于无锁状态等以后有需要时才在std::unique_lock对象不是互斥对象上调用lock()而获取锁或者把std::unique_lock对象交给std::lock()函数加锁。 std::call_once()令所有线程共同调用std::call_once()函数确保在该调用返回时指针初始化由其中某一线程安全且唯一地完成通过合适的同步机制。std::once_flag必要的同步数据由std::once_flag实例存储每个std::once_flag实例对应一次不同的初始化。