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 = ; j < ; ++j)
{
std::cout << i << " ";
}
std::cout << std::endl;
}
}; std::mutex m;
std::vector<std::thread> threads; for (int i = ; i < ; 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) //
{
std::lock_guard<std::mutex> lk(resource_mutex);
if (!resource_ptr) //
{
resource_ptr.reset(new some_resource); //
}
}
resource_ptr->do_something(); //
}

  C++的解决方法:

        std::shared_ptr<some_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();
}

  线程安全类成员的延迟初始化

        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) //
{
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();
}
};

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); //
std::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); //
entries[domain] = dns_details;
}
};

C++并发编程 互斥和同步的更多相关文章

  1. <<操作系统精髓与设计原理>>读书笔记(一) 并发性:互斥与同步(1)

    <<操作系统精髓与设计原理>>读书笔记(一) 并发性:互斥与同步 并发问题是所有问题的基础,也是操作系统设计的基础.并发包括很多设计问题,其中有进程间通信,资源共享与竞争,多个 ...

  2. 【Java并发编程二】同步容器和并发容器

    一.同步容器 在Java中,同步容器包括两个部分,一个是vector和HashTable,查看vector.HashTable的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并 ...

  3. 网络编程基础----并发编程 ---守护进程----同步锁 lock-----IPC机制----生产者消费者模型

    1  守护进程: 主进程 创建 守护进程   辅助主进程的运行 设置进程的 daemon属性 p1.daemon=True 1 守护进程会在主进程代码执行结束后就终止: 2 守护进程内无法再开启子进程 ...

  4. Python并发编程-进程 线程 同步锁 线程死锁和递归锁

    进程是最小的资源单位,线程是最小的执行单位 一.进程 进程:就是一个程序在一个数据集上的一次动态执行过程. 进程由三部分组成: 1.程序:我们编写的程序用来描述进程要完成哪些功能以及如何完成 2.数据 ...

  5. 并发编程(ReentrantLock&&同步模式之顺序控制)

    4.13 ReentrantLock 相对于 synchronized 它具备如下特点 可中断 可以设置超时时间 可以设置为公平锁 支持多个条件变量,即对与不满足条件的线程可以放到不同的集合中等待 与 ...

  6. 并发编程---互斥锁---互斥锁与join的区别

    互斥锁 互斥锁:就是把多个进程并发,修改成一块共享数据的操作变成串行,保证是一个一个来修改的. 缺点:效率低,加锁过程复杂 优点:增加了安全性 from multiprocessing import ...

  7. java并发编程:线程同步和锁

    一.锁的原理 java中每个对象都有一个内置锁.当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this)有关的锁.获得一个对象的锁也称为获取锁,当程序运 ...

  8. Java并发编程3-抽象同步队列AQS详解

    AQS是AtractQueuedSynchronizer(队列同步器)的简写,是用来构建锁或其他同步组件的基础框架.主要通过一个int类型的state来表示同步状态,内部有一个FIFO的同步队列来实现 ...

  9. java并发编程基础——线程同步

    线程同步 一.线程安全问题 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码.如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安 ...

随机推荐

  1. 第八次ScrumMeeting博客

    第八次ScrumMeeting博客 本次会议于11月2日(四)22时整在3公寓725房间召开,持续20分钟. 与会人员:刘畅.辛德泰.窦鑫泽.张安澜.赵奕.方科栋. 1. 每个人的工作(有Issue的 ...

  2. [leetcode-914-X of a Kind in a Deck of Cards]

    In a deck of cards, each card has an integer written on it. Return true if and only if you can choos ...

  3. Java 常用类String类、StringBuffer类

    常用类 String类.StringBuffer类 String代表不可变的字符序列 "xxxxxxx"为该类的对象 举例(1) public class Test { publi ...

  4. 中国剩余定理---FZU 1402 猪的安家

    J - 猪的安家 Time Limit:1000MS     Memory Limit:32768KB     64bit IO Format:%I64d & %I64u Submit Sta ...

  5. c文法

    程序→<外部声明>|<程序> 外部声明→<功能定义>|<声明> 功能定义→<声明复合语句的类型> 类型→<VOID| CHAR| IN ...

  6. PAT 甲级 1059 Prime Factors

    https://pintia.cn/problem-sets/994805342720868352/problems/994805415005503488 Given any positive int ...

  7. RPM 方式安装 Oracle18c 的方法

    1. 云和恩墨公众号介绍了 18c 通过rpm方式的安装包. 所以需要先下载一下. 地址. https://www.oracle.com/technetwork/database/enterprise ...

  8. static关键字的新用法

    static关键字的新用法和总结: static这个关键字,也可以像“self”一样,代表“当前类”,用于访问一个类的“静态属性或静态方法”: 但, static,在应用中,更灵活,因此更常见! 因为 ...

  9. Hadoop RPC protocol description--转

    原文地址:https://spotify.github.io/snakebite/hadoop_rpc.html Snakebite currently implements the followin ...

  10. BZOJ 2004 公交线路(状压DP+矩阵快速幂)

    注意到每个路线相邻车站的距离不超过K,也就是说我们可以对连续K个车站的状态进行状压. 然后状压DP一下,用矩阵快速幂加速运算即可. #include <stdio.h> #include ...