C++并发编程实战学习笔记

本文详细介绍了C++中的并发编程实践,包括线程管理、线程函数传递参数、启动线程、线程间共享数据的保护、同步并发操作如条件变量和future的使用,以及内存模型和原子操作的原理和应用。
C++并发编程实战
入门
线程管理
基础
线程函数传递参数
线程启动
线程等待
后台线程
转移所有权
确定线程数量
线程标识
线程间共享数据
共享数据问题
互斥量保护共享数据
保护共享数据的其它方法
同步并发操作
等待一个事件或者其他条件
使用期望等待一次性事件
async
packaged_task/future
promise/future
限时等待
使用同步操作简化代码
内存模式和原子操作
内存模型
内存布局
并发
基于锁的数据结构
无锁数据结构
设计并发代码
高级线程管理
线程池
线程中断

入门

包含头文件<thread>
类thread

线程管理

启动线程std::thread

  • 可调用对象构造
    • 普通函数、函数指针或者函数引用
    • 仿函数对象(实现了operator())
    • lambda表达式
    • 函数适配器(std::bind, std::function, mem_fn)
    • 成员函数(静态成员函数、非静态成员函数)
  • 构造函数传入临时变量可能导致“最令人头痛的语法解析”
    • 双括号
    • {}初始化
    • lambda表达式

使用仿函数的例子

#include <iostream>
#include <thread>

using namespace std;

void hello()
{
	cout << "Hello Concurrent World\n";
}

class background_task
{
public:
	void operator()()
	{
		cout << "background_task" << endl;
	}
};

int main()
{
	background_task task;
	thread t(task);

	t.join();
}

在线程对象销毁前决定

  • 线程分离detach
  • 线程等待
    • join
      • RAII+join: join前判断joinable
      • 不可重复 join
    • 其它机制
      • 条件变量
      • 期待(futures)

后台线程

使用detach分离线程

  • 可能的问题:局部变量所在的线程结束,局部变量已经销毁,后台线程还在使用这个局部变量
  • 分离后thread对象与实际执行的线程已经无关
  • 分离的线程不能joinable
  • 不能对没有执行线程的 std::thread 对象使用detach(),可以用 joinable 检查
  • 在detach下要避免隐式转换,因为此时子线程可能还来不及转换主线程就结束了,应该在构造线程时,用参数构造一个临时对象传入

线程传参

在传递指针或者引用时需要注意。对于后台线程时,因为局部变量的指针作用域已经结束,而线程还是处理传参时,会出现未定义的行为


void f(int i, string const& s);

void oops(int some_param)
{
	char buffer[1024];
	sprintf(buffer, "%i", some_param);
	thread t(f, 3, buffer);
	t.detach();
}

因为,无法保证隐式转换的操作和 std::thread 构造函数的拷贝操作的顺序,有可能 std::thread 的构造函数拷贝的是转换前的变量(buffer指针)。解决方案就是在传递到 std::thread 构造函数之前,就将字面值转化为 std::string
对于传非常引用时,直接传值会报错

void udpate_data_for_widget(widget_data& data);

void oops()
{
	widget_data data;
	thread t(udpate_data_for_widget, data);
	t.join();
}

在这里插入图片描述
需要改成ref

void udpate_data_for_widget(widget_data& data);

void oops()
{
	widget_data data;
	thread t(udpate_data_for_widget, ref(data));
	t.join();
}

转移所有权

使用move来转移
如果转移到一个关联的线程,会抛出异常

#include <iostream>
#include <thread>
#include <string>

using namespace std;

void some_function()
{

}

void some_other_function()
{

}

int main()
{
	thread t1(some_function);
	thread t2 = move(t1);
	t1 = thread(some_other_function);
	thread t3;
	t3 = move(t2);
	t1 = move(t3);//抛出异常
	
	return 0;
}

确定线程数量

静态方法thread::hardware_concurrency()
如果没有定义,则返回0

线程标识

std::thread::id
get_id() 获取

线程间共享数据

共享数据问题

  • 条件竟争
  • 避免条件竞争
    • 对数据结构采用保护机制,保证只有进行修改的线程才能看到不变量被破坏时的中间状态
    • 对数据结构和不变量的设计进行修改,修改完的结构必须能完成一系列不可分割的变化 。即无锁编程(lock-free)
    • 使用事务的方法去处理数据结构的更新

互斥量保护共享数据

需要头文件<mutex>

RAII 互斥锁

  • lock_guard
  • scoped_lock

检查指针和引用(可能可以绕过保护机制)

  • 没有成员函数通过返回值或者输出参数的形式向调用者返回指向受操作数据的指针或者引用
  • 检查成员函数是否通过指针或者引用的方式来调用
    • 只是将所有可访问的数据结构代码标记为互斥等于没有保护,由开发者正确使用
  • 切勿将受保护数据的指针或引用传递到互斥锁作用域之外

接口间的条件竞争

  • 问题发生在接口设计上,需要修改接口设计
  • 削减接口可以获得最大程度的安全,甚至对操作作些限制

避免死锁的建议和策略

可以使用下面方式避免死锁

lock(mutex1, mutex2);
lock_guard<mutex> lock_a(mutex1, adopt_lock);
lock_guard<mutex> lock_b(mutex2, adopt_lock);

或者使用c++17中的scoped_lock

scoped_lock guard(mutex1, mutex2);
  • 避免嵌套锁
  • 避免在持有锁时调用外部代码
  • 使用固定顺序获取锁
    • 定义遍历的顺序,一个线程必须先锁住A才能获取B的锁,在锁住B之后才能获取C的锁
    • std::lock 内部使用死锁避免算法,可以同时锁著多个互斥量,可以搭配 lock_guard 使用(scoped_lock)
  • 使用层次锁结构

std::unique_lock——灵活的锁

  • 比 lock_guard 灵活,但对象的体积更大了
  • 支持lock(), try_lock()和unlock()
  • 可以移动但不可赋值

保护共享数据的其它方法

保护共享数据的初始化过程

  • 延迟初始化
  • 声名狼藉的双重检查锁模式
  • call_once 和 once_flag, mutex 和 once_flag 的实例不能拷贝和移动
  • 在只需要一个全局实例的情况下,可以使用C++11中的局部static对象来代替 call_once

保护不常更新的数据结构(读多写少)

  • 读写锁:c++17中的shared_mutex,shared_timed_mutex
    • 读使用shared_lock<shared_mutex>
    • 写使用lock_guard<shared_mutex>或者unique_lock<shared_mutex>
  • 性能
    • 依赖于参与其中的处理器数量
    • 与读者和作者线程的负载有关
  • 限制
    • 当有线程拥有共享锁时,尝试获取独占锁的线程会被阻塞,直到所有其他线程放弃锁
    • 当任一线程拥有一个独占锁时,其他线程就无法获得共享锁或独占锁,直到第一个线程放弃其拥有的锁
  • 嵌套锁
    • 用于成员函数间调用时,并且每个成员函数有作保护时,可以使用recursive_mutex

同步并发操作

等待一个事件或者其他条件

选择

  • 自旋,持续检查
  • 在检查间隙周期性的间歇,即使用this_thread::sleep_for
  • 条件变量,即condition_variable或者condition_variable_any,需要头文件<condition_variable>

使用期望等待一次性事件

  • 一次性事件future
    需要头文件<future>。分为两类,unique future即std::future<>和shared futures即shared_future<>,future只能与指定事件关联,shared_future可以关联多个事件
  • async函数模板:后台任务返回值
  • package_task 任务与期望关联:打包任务,可以在线程间传递任务
  • promise 与期望关联
  • 将异常存与future中
  • 多个线程的等待 std::shared_future:让每个线程都拥有自己的 shared_future 拷贝对象,这样就可以让多线程访问共享同步结果安全

限时等待

  • 时钟类,头文件<chrono>
    • 当前时间,now()
    • 时间类型
    • 时钟节拍,ratio<nominator, denominator>
    • 稳定时钟,system_clock不是稳定时钟,steady_clock是稳定时钟,high_resolution_clock高精度时钟
  • 时间段,duration<>
    时间段类型有
    • nanoseconds
    • microseconds
    • milliseconds
    • seconds
    • minutes
    • hours
  • 时间点,time_point<>
类型/命名空间函数
std::this_threadsleep_for(duration)/sleep_until(time_point)
std::condition_variable/std::condition_variable_anywait_for(lock, duration)/wait_until(lock,time_point) /wait_for(lock,duration, predicate)/wait_until(lock,time_point, predicate)
timed_mutex/recursive_timed_mutextry_lock_for(duration)/try_lock_until(time_point)
std::unique_lock<timedlockable>unique_lock(lockable, duration)/unique_lock(lockable, time_point)/try_lock_for(duration)/try_lock_until(time_point)
std::future/std::shared_futurewait_for(duration)/wait_until(time_point)

使用同步操作简化代码

  • 使用future的函数化编程
  • 使用消息传递的同步操作
  • 扩展规范中的持续性并发
  • 持续性连接
  • 等待多个future:when_all
  • 等待第一个future:when_any
  • 并发技术扩展规范中的锁存器和栅栏机制

内存模型和原子操作

原子类型

需要头文件 <atomic>
成员函数is_lock_free(),如果是直接用的原子指令,返回true,如果是使用了锁结构,则返回 false
使用互斥量实现的原子操作对性能不会提升
标准原子类型不能拷贝和赋值,但是可以隐式转化居对应的内置类型,赋值使用load(),store(),exchange(),compare_exchange_weak(), compare_exchange_strong()
compare_exchange_strong,compare_exchange_weak原型为

bool compare_exchange_weak( T& expected, T desired,std::memory_order order = std::memory_order_seq_cst ) noexcept;
  • 如果原子对象的值与expected,则将原子对象的值修改为desired
  • 如果不相等,则将expected的值修改为原子 对象的值

操作对应的操作时序

操作时序
loadmemory_order_relaxed,memory_order_consume,memory_order_acquire,memory_order_seq_cst
storememory_order_relaxed,memory_order_release,memory_order_seq_cst
read-modity-writememory_order_relaxed,memory_order_consume,memory_order_acquire,memory_order_release,memory_order_acq_rel,memory_order_seq_cst

内存次序分为三种模式

  • 先后一致次序:memory_order_seq_cst, 保证所有线程看到的内存操作顺序是一致的
  • 获取-释放次序:memory_order_consume,memory_order_acquire,memory_order_release,memory_order_acq_rel,memory_order_acquire是在读操作之后插入LoadLoad屏障,确保屏障之后的操作不会重排到屏障之前;memory_order_release是在写操作前插入StoreStore屏障,确保屏障之前的操作不会重排到屏障之后;memory_order_acq_rel是同时插入StoreStore和LoadLoad屏障,memory_order_consume是确保前序依赖和携带依赖的不会重排到屏障前面
  • 宽松次序:memory_order_relaxed

原子类型相关宏

ATOMIC_BOOL_LOCK_FREE
ATOMIC_CHAR_LOCK_FREE
ATOMIC_CHAR16_T_LOCK_FREE
ATOMIC_CHAR32_T_LOCK_FREE
ATOMIC_WCHAR_T_LOCK_FREE
ATOMIC_SHORT_LOCK_FREE
ATOMIC_INT_LOCK_FREE
ATOMIC_LONG_LOCK_FREE
ATOMIC_LLONG_LOCK_FREE
ATOMIC_POINTER_LOCK_FREE

上面的宏,如果原子类型不是无锁结构,值为0,如果是无锁结构,值为2,如果无锁状态在运行时才能确定,值为1

  • atomic_flag
    没有is_lock_free()方法
    必须初始化为ATOMIC_FLAG_INIT
    支持两个操作,销毁clear(),清除或设置test_and_set()

C++标准库 – 读写锁的实现

基于锁的数据结构

  • 队列:通过使用预分配一个虚位节点方式。队列为空时,包含一个虚位结点

无锁数据结构

  • 无锁栈,void pop(T& val)因为其不是异常安全的,其是先弹出节点,然后赋值,如果赋值抛出异常,容器结构变了;在换成shared_ptr<T> pop()时,直接删除转换出的头结点可能会出现异常,因为其它线程可能持有这个被删除的结点,在head.compare_and_exchange_weak(old_head, old_head->next),在访问old_head->next可能会抛出异常,所以需要延时删除
    • 使用threads_in_pop原子计数,在高负载情况下,会导致to_be_deleted链表过长
    • 使用疯险指针,pop时需要全扫描疯险指针数组
    • 外内部分别使用引用计数

设计并发代码

切分任务方法

  • 先切分数据,再线程处理
  • 递归切分数据,即线程处理过程中再划分创建新线程处理
  • 工作类别划分任务

高级线程管理

线程池

  • 等待提交给线程池的任务完成执行
  • 线程池中区分线程独有队列和全局队列
  • 任务窃取队列

线程中断

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kgduu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值