C++并发编程 异步任务(async)
线程基本的互斥和同步工具类, 主要包括:
std::mutex 类
std::recursive_mutex 类
std::timed_mutex 类
std::recursive_timed_mutex 类
std::lock_guard 类型模板
std::unique_lock 类型模板
std::lock 函数模板
std::once_flag 类
std::call_once 函数模板std::mutex 类
std::mutex 上锁须要调用 lock() 或 try_lock(), 当有一个线程获取了锁, 其它线程想要取得此对象的锁时, 会被阻塞(lock)或失败(try_lock). 当线程完成共享数据的保护后, 需要调用 unlock 进行释放锁.
std::mutex 不支嵌套, 如果两次调用 lock, 会产生未定义行为.std::recursive_mutex 类
使用方法同 std::mutex, 但 std::recursive_mutex 支持一个线程获取同一个互斥量多次,而没有对其进行一次释放. 但是同一个线程内, lock 与 unlock 次数要相等, 否则其它线程将不能取得任何机会.
其原理是, 调用 lock 时, 当调用线程已持有锁时, 计数加1; 调用 try_lock 时, 尝试取得锁, 失败时不会阻塞, 成功时计数加1; 调用 unlock 时, 计数减1, 如果是最后一个锁时, 释放锁.
需要注意的是: 调用 try_lock时, 如果当前线程未取得锁, 即使没有别的线程取得锁, 也有可能失败.std::timed_mutex 类
std::timed_mutex 在 std::mutex 的基础上支持让锁超时. 上锁时可以调用 try_lock_for, try_lock_until 设置超时值.
try_lock_for 的参数是需要等待的时间, 当参数小于等于0时会立即返回, 效果和使用 try_lock 一样.
try_lock_until 传入的参数不能小于当前时间, 否则会立即返回, 效果和使用 try_lock 一样. 实际上 try_lock_for 内部也是调用 try_lock_until 实现的.
tm.try_lock_for(std::chrono::milliseconds(1000)) 与 tm.try_lock_until(std::chrono::steady_clock::now() + std::chrono::milliseconds(1000)) 等价, 都是等待1s.std::recursive_timed_mutex 类
std::recursive_timed_mutex 在 std::recursive_mutex 的基础上, 让锁支持超时.
用法同 std::timed_mutex, 超时原理同 std::recursive_mutex.std::lock_guard 类型模板
std::lock_guard 类型模板为基础锁包装所有权. 指定的互斥量在构造函数中上锁, 在析构函数中解锁.
这就为互斥量锁部分代码提供了一个简单的方式: 当程序运行完成时, 阻塞解除, 互斥量解锁(无论是执行到最后, 还是通过控制流语句break或return, 亦或是抛出异常).
std::lock_guard 不支持拷贝构造, 拷贝赋值和移动构造.std::unique_lock 类型模板
std::unique_lock 类型模板比 std::loc_guard 提供了更通用的所有权包装器.
std::unique_lock 可以调用 unlock 释放锁, 而后当再次需要对共享数据进行访问时再调用 lock(), 但是必须注意一次 lock 对应一次 unlock, 不能连续多次调用同一个 lock 或 unlock.
std::unique_lock 不支持拷贝构造和拷贝赋值, 但是支持移动构造和移动赋值.
std::unique_lock 比 std::loc_guard 还增加了另外几种构造方式:
unique_lock(_Mutex& _Mtx, adopt_lock_t) 构建持有锁实例, 其不会调用 lock 或 try_lock, 但析构时默认会调用 unlock.
unique_lock(_Mutex& _Mtx, defer_lock_t) 构建非持有锁实例, 其不会调用 lock 或 try_lock, 如果没有使用 std::lock 等函数修改标志, 析构时也不会调用 unlock.
unique_lock(_Mutex& _Mtx, try_to_lock_t) 尝试从互斥量上获取锁, 通过调用 try_lock
unique_lock(_Mutex& _Mtx, const chrono::duration<_Rep, _Period>& _Rel_time) 在给定时间长度内尝试获取锁
unique_lock(_Mutex& _Mtx, const chrono::time_point<_Clock, _Duration>& _Abs_time) 在给定时间点内尝试获取锁
bool owns_lock() const 检查是否拥有一个互斥量上的锁std::lock 函数模板
std::lock 函数模板提供同时锁住多个互斥量的功能, 且不会有因改变锁的一致性而导致的死锁. 其声明如下:
template<typename LockableType1,typename... LockableType2> void lock(LockableType1& m1,LockableType2& m2...);// 使用互斥量保护代码typedef std::lock_guard<std::mutex> MutexLockGuard;typedef std::unique_lock<std::mutex>UniqueLockGuard;class Func{int i;std::mutex& m;public:Func(int i_, std::mutex& m_) : i(i_), m(m_) {}void operator() (){//MutexLockGuard lk(m);UniqueLockGuard lk(m);for (unsigned j = 0; j < 10; ++j){std::cout << i << " ";}std::cout << std::endl;}};std::mutex m;std::vector<std::thread> threads;for (int i = 1; i < 10; i++){Func f(i, m);threads.push_back(std::thread(f));}std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join)); // 对每个线程调用join() // 同时对多个 mutex 上锁std::mutex m1;std::mutex m2;//std::unique_lock<std::mutex> lock_a(m1, std::defer_lock);//std::unique_lock<std::mutex> lock_b(m2, std::defer_lock); // std::def_lock 留下未上锁的互斥量//std::lock(lock_a, lock_b); // 互斥量在这里上锁, 并修改对象的上锁标志std::lock(m1, m2); // 锁住两个互斥量std::lock_guard<std::mutex> lock_a(m1, std::adopt_lock); // std::adopt_lock 参数表示对象已经上锁,因此不会调用 lock 函数std::lock_guard<std::mutex> lock_b(m2, std::adopt_lock); std::call_once 函数模板
如果多个线程需要同时调用某个函数,std::call_once 可以保证多个线程对该函数只调用一次, 并且是线程安全的.线程安全的延迟初始化
-- 使用 std::call_once 和 std::once_flag 考虑下面的代码, 每个线程必须等待互斥量,以便确定数据源已经初始化, 这导致了线程资源产生不必要的序列化问题.std::shared_ptr<some_resource> resource_ptr;std::mutex resource_mutex;void foo(){std::unique_lock<std::mutex> lk(resource_mutex); // 所有线程在此序列化if (!resource_ptr){resource_ptr.reset(new some_resource); // 只有初始化过程需要保护}lk.unlock();resource_ptr->do_something();}使用双重检查锁优化上述代码, 指针第一次读取数据不需要获取锁, 并且只有在指针为NULL时才需要获取锁; 然后, 当获取锁之后, 指针会被再次检查一遍(这就是双重检查的部分), 避免另一的线程在第一次检查后再做初始化, 并且让当前线程获取锁.
这样同样存在问题, 即潜在的条件竞争, 因为外部的读取锁①时没有与内部的写入锁进行同步③, 因此就会产生条件竞争,这个条件竞争不仅覆盖指针本身, 还会影响到其指向的对象:
即使一个线程知道另一个线程完成对指针进行写入, 它可能没有看到新创建的some_resource实例, 然后调用do_something()④后, 得到不正确的结果. 这在C++标准中被指定为“未定义行为”.void undefined_behaviour_with_double_checked_locking(){if (!resource_ptr) // 1{std::lock_guard<std::mutex> lk(resource_mutex);if (!resource_ptr) // 2{resource_ptr.reset(new some_resource); // 3}}resource_ptr->do_something(); // 4} C++的解决方法:std::shared_ptr<some_resource> resource_ptr;std::once_flag resource_flag; // 1void init_resource(){resource_ptr.reset(new some_resource);}void foo(){std::call_once(resource_flag,init_resource); // 可以完整的进行一次初始化resource_ptr->do_something();} 线程安全类成员的延迟初始化class 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) // 1{std::call_once(connection_init_flag, &X::open_connection, this); // 2connection.send_data(data);}data_packet receive_data() // 3{std::call_once(connection_init_flag, &X::open_connection, this); // 2return connection.receive_data();}};boost::shared_lock
读者-写者锁 boost::shared_lock, 允许两中不同的使用方式:一个“作者”线程独占访问和共享访问, 让多个“读者”线程并发访问. (C++11标准不支持)
其性能依赖与参与其中的处理器数量, 也与读者和写者线程的负载有关. 一种典型的应用:#include <map>#include <string>#include <mutex>#include <boost/thread/shared_mutex.hpp>class dns_entry;class dns_cache{std::map<std::string, dns_entry> entries;mutable boost::shared_mutex entry_mutex;public:dns_entry find_entry(std::string const& domain) const{boost::shared_lock<boost::shared_mutex> lk(entry_mutex); // 1std::map<std::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_guard<boost::shared_mutex> lk(entry_mutex); // 2entries[domain] = dns_details;}};
本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-12/138002.htm