std::this_thread::sleep_for()
函数,在各次查验之间短期休眠。
std::condition_variable
std::condition_variable
仅限于与 std::mutex
一起使用std::condition_variable_any
std::condition_variable
应予优先采用,除非有必要令程序更灵活
内
std::future<>
)std::shared_future<>
)std::future
和 std::shared_future
。std::experimental
名字空间中给出了上述类模板的扩展版本:std::experimental::future<>
和 std::experimental::shared_future<>
。
std::thread
没有提供直接回传结果的方法,所以函数模板 std::async()
应运而生
中。std::future
取得异步任务的函数返回值int find_the_answer_to_ltuae();
void do_other_stuff();
....
std::future<int> the_answer=std::async(find_the_answer_to_ltuae);
do_other_stuff();
std::cout<<"The answer is "<<the_answer.get()<<std::endl;
std::async()
补充一个参数,以指定采用哪种运行方式, 参数的类型是 std::launch
。
std::launch::deferred
wait()
或 get()
,任务函数才会执行std::launch::async
std::launch::deferred | std::launch:: async
auto f=std::async(std::launch::deferred,func,std::ref(parame));
std::packaged_task<>
连结了 future 对象与函数(或可调用对象)
std::packaged_task<>
对象在执行任务时,会调用关联的函数(或可调用对象),把返回值保存为 future 的内部数据,并令 future 准备就绪std::packaged_task<>
实例之中,再传递给任务调度器或线程池std::packaged_task<>
具有成员函数 get_future()
,它返回 std::future<>
实例std::packaged_task<>
特化示例:template<>
class packaged_task<std::string(std::vector<char>*,int)>
{
public:
template<typename Callable>
explicit packaged_task(Callable&& f);
std::future<std::string> get_future();
void operator()(std::vector<char>*,int);
};
std::packaged_task
对象是可调用对象
std::thread
对象std::packaged_task
实现std::promise
给出了一种异步求值的方法(类型为T)
std::future
对象与结果关联,能延后读出需要求取的值std::promise
和 std::future
可实现下面的工作机制
get_future()
set_value()
若经由 std::async()
调用的函数抛出异常,则会被保存到 future 中
get()
被调用get()
后存储在内的异常即被重新抛出std::promise
也具有同样的功能它通过成员函数的显式调用实现。
set_value()
set_exception()
extern std::promise<double> some_promise;
try
{
some_promise.set_value(calculate_value());
}
catch(...)
{
//std::current_exception() 用于捕获抛出的异常
some_promise.set_exception(std::current_exception());
}
some_promise.set_exception(std::make_exception_ptr(std::logic_error("foo ")))
;另一种方法保存异常到 future 中:
set()
成员函数,也不执行包装的任务,而直接销毁与 future 关联的 std::promise
对象或 std::packaged_task
对象std::future_error
存储为异步任务的状态数据std::future_errc::broken_promise
future自身存在限制
std::shared_future
。std::future
特性
get()
仅能被有效调用唯一一次get()
会进行移动操作,之后该值不复存在只要同步操作是一对一地在线程间传递数据,std::future
就都能处理
std::shared_future
对象的副本,它们为各线程独自所有,并被视作局部变量valid()
,用于判别异步状态是否有效。std::shared_future
,实现并行处理。每个单元格的终值都唯一确定,并可以用于其他单元格的计算公式std::shared_future
std::promise<std::string> p;
std::shared_future<std::string> sf(p.get_future());
std::condition_variable
含有成员函数 wait_for()
和 wait_until()
std::chrono::system_clock::now()
可返回系统时钟的当前时刻time_point
成员类型(member type),它是该时钟类自有的时间点类some_clock::now()
的返回值的类型就是 some_clock::time_point
std::ratio<1,25>
std::ratio<5,2>
is_steady
std::chrono::system_clock
类不是恒稳时钟,因为它可调整。now()
,后来返回的时间值甚至早于前一个std::chrono::steady_clock
std::chrono::system_clock
from_time_t()
和 to_time_t()
std::chrono::high_resolution_clock
std::chrono::duration<>
是标准库中最简单的时间部件
std::chrono::duration>
std::chrono::duration>
std::chrono::duration>
using namespace std::chrono_literals;
auto one_day=24h;
auto half_an_hour=30min;
auto max_time_between_messages=30ms;
std::chrono::duration_cast<>
完成std::chrono::duration<>
实例//等待某个future进入就绪状态,并以35毫秒为限
std::future<int> f=std::async(some_task);
if(f.wait_for(std::chrono::milliseconds(35))==std::future_status::ready)
do_something_with(f.get());
std::future_status::timeout
std::future_status::ready
std::future_status::deferred
std::chrono::time_point<>
的实例表示
time_since_epoch()
std::chrono::high_resolution_clock::now() + std::chrono::nanoseconds(500)
auto start=std::chrono::high_resolution_clock::now();
do_something();
auto stop=std::chrono::high_resolution_clock::now();
std::cout<<"do_something() took "
<(stop-start).count()
<<" seconds"<
- 静态函数
std::chrono::system_clock::to_time_point()
转换 time_t 值
- 求出基于系统时钟的时间点
- 最优解
- 在程序代码中的某个固定位置,将
some_clock::now()
和前向偏移相加得出时间点
- 条件变量进行限时等待
- Code_4_3_3
std::condition_variable cv;
bool done;
std::mutex m;
bool wait_loop()
{
auto const timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
std::unique_lock<std::mutex> lk(m);
while (!done)
{
if (cv.wait_until(lk, timeout) == std::cv_status::timeout)
break;
}
return done;
}
4.3.4 接受超时时限的函数
-
超时时限的最简单用途是,推迟特定线程的处理过程,若它无所事事,就不会占用其他线程的处理时间
-
std::this_thread::sleep_for()
- 线程或采用 sleep_for() 按指定的时长休眠
-
std::this_thread::sleep_until()
- 休眠直到指定时刻为止
-
普通的 std::mutex
和 std::recursive_mutex
不能限时加锁
-
std::timed_mutex
和 std::recursive_timed_mutex
可以限时加锁
- 这两种锁都含有成员函数
try_lock_for()
和 try_lock_until()
- 前者尝试在给定的时长内获取锁
- 后者尝试在给定的时间点之前获取锁
-
C++ 标准库中接受超时时限的函数
std::this_thread
sleep_for
sleep_unitl
- 无返回值
std::condition_variable
或 std::condition_variable_any
wait_for(lock, duration)
wait_until(lock, time_point)
- 被唤醒时断言的返回值–bool
std::cv_status::timeout
或 std::cv_status::no_timeout
wait_for(lock, duration, predicate)
wait_until(lock, time_point, predicate)
- 被唤醒时断言的返回值–bool
-
std::timed_mutex
, std::recursive_timed_mutex
或 std::shared_timed_mutex
try_lock_for(duration)
try_lock_until(time_point)
- 若获取了锁,则返回 true,否则返回 false
-
std::shared_timed_mutex
try_lock_shared_for(duration)
try_lock_shared_until(time_point)
- 若获取了锁,则返回 true,否则返回 false
-
std::unique_lock
, unique_lock(lockable, duration)
或 unique_lock(lockable, time_point)
try_lock_for(duration)
try_lock_until(time_point)
- 如果在新构建的对象上获取了锁,那么
owns_lock()
返回 true,否则返回 false
- 若获取了锁,则返回 true,否则返回 false
-
std::shared_lock
, shared_lock(lockable,duration)
或 shared_lock(lockable,time_point)
try_lock_for(duration)
try_lock_until(time_point)
- 如果在新构建的对象上获取了锁,那么
owns_lock()
返回 true,否则返回 false
- 若获取了锁,则返回 true,否则返回 false
-
std::future
或 std::shared_future
wait_for(duration)
wait_until(time_point)
- 如果等待超时则返回
std::future_status::timeout
- 如果 future 已就绪则返回
std::future_status::ready
- 如果 future 上的函数按推迟方式执行,且尚未开始执行,则返回
std::future_status::deferred
4.4 运用同步操作简化代码
- 线程间不会直接共享数据,而是由各任务分别预先备妥自己所需的数据,并借助 future 将结果发送到其他有需要的线程
4.4.1 利用 future 进行函数式编程
- 函数式编程”(functional programming)是指一种编程风格,函数调用的结果完全取决于参数,而不依赖任何外部状态。
- 函数式语言所含的要素大多数是纯的,真正改动共享状态的是非纯函数
- 若要以 C++ 实现函数式编程风格的并发编程,future 则是画龙点睛之笔使之真正切实可行
- future 对象可在线程间传递,所以一个计算任务可以依赖另一个任务的结果,却不必显式地访问共享数据。
- 函数式编程风格的快速排序
- Code_4_4_1_1
- 函数式编程风格的并行快速排序 (
std::async()
)
- Code_4_4_1_2
std::async()
每次都开启新线程,那么只要递归3层,就会有8个线程同时运行
- 如果递归10层(考虑约1000个元素的情形),将有1024个线程同时运行
- 一旦线程库断定所产生的任务过多,就有可能转为按同步方式生成新任务
- 函数式编程风格的并行快速排序 (
spawn_task()
)
- 使用 packaged_task
- Code_4_4_1_3
std::result_of
被弃用,这个函数需要重写
- C++17 标准库给出了快速排序的并发重载版本
- 全面把握线程池的构建细节,并绝对掌握线程池执行任务的方式。
- 只有这样,才值得让
std::async()
退下“火线”,而优先采用线程池模式
- 并发编程范式存在多种风格
- 函数式编程是其中之一
- 它能够摆脱共享的可变数据
- 通信式串行进程 (Communicating Sequential Process,CSP)
- 线程相互完全隔离,没有共享数据,采用通信管道传递消息
- 编程语言Erlang和MPI[27]编程环境都采用了这种范式
4.4.2 使用消息传递进行同步
- CSP的理念
- 假设不存在共享数据,线程只接收消息
- 那么单纯地依据其反应行为,就能独立地对线程进行完整的逻辑推断。
- CSP线程实际上都与状态机(state machine)等效
- 无论我们采用什么方法实现 CSP 线程,只要将它们分割出去,视作独立进程,就能让并发行为摆脱共享数据,从而大有可能消除大部分复杂性,使程序更容易编写,并且降低错误率
- 真正的 CSP 模型没有共享数据,全部通信都经由消息队列传递
- 但是 C++ 线程共享地址空间,因而这一规定无法强制实施
- 我们必须恪守规定,确保剔除线程间的共享数据。
- 作为线程间通信的唯一途径,消息队列必须共享,但细节要由程序库封装并隐藏
- 模拟自动柜员机逻辑的一种方式是状态机
- 等待-处理-转移
- Code_4_4_2_1
- 附录 C 将提供完整的代码,简单地实现自动柜员机系统
- 上例还示范了“分离关注点”的软件设计原则
- 通过利用多个线程,整体任务按要求被明确划分。
- 使用消息传递编写程序
- 无须再忧虑同步和并发的问题
- 在某个具体的状态下,仅仅专注于所应该收发的消息即可
- CSP风格的编程可大幅简化并发系统的设计工作,因为我们可以完全独立地处理每个线程
4.4.3 符合并发技术规约的后续风格并发
- 并发技术规约在名字空间std::experimental内
- 给出了对应
std::promise
和 std:: packaged_task
的新版本
- 它们都返回
std::experimental::future
实例,而非 std::future
- 一旦结果数据就绪,就接着进行某项处理”,这正是后续的功能。
- 为 future 添加后续调用的成员函数名为 then()。
std::experimental::future<int> find_the_answer;
auto fut=find_the_answer();
auto fut2=fut.then(find_the_question);
assert(!fut.valid());
assert(fut2.valid());
- 一旦最开始的 future 准备就绪, 后续函数
find_the_question()
会在“某一线程上”运行
- 无法确定具体是哪个线程
- 无法向后续函数传递参数
- 因为参数已经由程序库预设好,先前准备就绪的 future 会传入后续函数,它所包含的结果会触发后续函数的调用
4.4.4 后续函数的连锁调用
- 登录模块设计
- 虽然异步可以防止主线程卡死,但是交由一个后台线程任务也很重。因此,可以使用连锁调用,开启多个线程
- Code_4_4_4
4.4.5 等待多个future
- 分发收集模式、类似 map reduce
- Code_4_4_5
when_any()
胜任如下情形
- 为了充分利用可调配的并发资源,我们生成多个任务同时运行,但只要其中一个完成运行,我们就需马上另外处理该项最先得出的结果
4.4.6 运用std::experimental::when_any()函数等待多个future,直到其中之一准备就绪
- 我们可以生成多个线程,它们分别查找数据集的子集;若有线程找到了符合条件的值,就设立标志示意其他线程停止查找,并设置最终结果的值
- Code_4_4_6
- 其涉及
std::experimental::when_any()
和 std::experimental::when_all()
的全部使用方式:
- 两者都通过容器将多个
std::experimental::future
移动复制到函数中,而且它们都以传值的方式接收参数,所以我们需要显式地向函数内部移动 future,或传递临时变量。
4.4.7 线程闩和线程卡——并发技术规约提出的新特性
- 线程闩(latch)和线程卡(barrier)的含义
- 线程闩是一个同步对象,内含计数器,一旦减到0,就会进入就绪状态
- 同一线程能令线程闩计数器多次减持,而多个线程也可分别令其计数器减持一次,或者两者兼有
- 线程卡是可重复使用的同步构件,针对一组给定的线程,在它们之间进行同步。
- 每个同步周期内,只准许每个线程唯一一次运行到其所在之处
4.4.8 基本的线程闩类 std::experimental::latch
std::experimental::latch
由头文件
定
- 等待目标事件数目
- 接收唯一一个参数,在构建该类对象时,我们需通过这个参数设定其计数器的初值
- 每当等待的目标事件发生时,我们就在线程闩对象上调用
count_down()
,一旦计数器减到 0,它就进入就绪状态
- 若要等待线程闩的状态变为就绪,则在其上调用
wait()
- 若需检查其是否已经就绪,则调用
is_ready()
- 要使计数器减持,同时要等待它减到0,则应该调用
count_down_and_wait()
void foo(){
unsigned const thread_count=...;
//构建线程闩对象done
latch done(thread_count);
my_data data[thread_count];
std::vector<std::future<void> > threads;
for(unsigned i=0;i<thread_count;++i)
//用std::async()发起相同数量的线程
threads.push_back(std::async(std::launch::async,[&,i]{
//各线程负责生成相关的数据块
data[i]=make_data(i);
//在完成时即令计数器减持
done.count_down();
//进行下一步处理
do_more_stuff();
}));
//主线程在处理生成的数据之前,只要在线程闩上等待,就能等到全部数据准备完成
done.wait();
process_data(data,thread_count);
//std::future 的析构函数会发生自动调用,这保证了前面所启动的线程全都会结束运行
}
4.4.9 基本的线程卡类 std::experimental::barrier
- 并发技术规约提出了两种线程卡
std::experimental::barrier
std::experimental::flex_barrier
- 头文件
中定义
- 前者相对简单,因而额外开销可能较低,而后者更加灵活,但额外开销可能较高
std::experimental::barrier
针对的场景
- 假定有一组线程在协同处理某些数据,各线程相互独立,分别处理数据,因此操作过程不必同步。
- 但有在全部线程都完成各自的处理后才可以操作下一项数据或开始后续处理
- 通过在线程卡对象上调用
arrive_and_wait()
等待同步组的其他线程
- 只要组内最后一个线程也运行至此,所有线程即被释放,线程卡会自我重置
- 只要在线程卡上调用
arrive_and_drop()
,即可令线程显式地脱离其同步组
- 线程闩的意义在于关闸拦截
- 一旦它进入了就绪状态,就始终保持不变
- 而线程卡则不同
- 线程卡会释放等待的线程并且自我重置,因此它们可重复使用
- 线程卡只与一组固定的线程同步,若某线程不属于同步组,它就不会被阻拦,亦无须等待相关的线程卡变为就绪
- 利用线程卡同一步组线程
- Code_4_4_9
4.4.10 std::experimental::flex_barrier——std::experimental::barrier的灵活版本
- 设定串行区域
- 当所有线程运行至该线程卡处时,区域内的代码就会接着运行,直到完成后全部线程才会释放。
std::experimental::flex_barrier
类的接口与 std::experimental::barrier
类的不同之处仅仅在于
- 前者具备另一个构造函数,其参数既接收线程的数目,还接收补全函数(completion function)
- Code_4_4_10 为 Code_4_4_9 的改版
- 我们通过补全函数设定串行区域,其功能相当强大,还能改变参与同步的线程数目
总结
关键 API
- std::this_thread::sleep_for()
- std::condition_variable 和 std::condition_variable_any
- std::future<> and std::shared_future<>
- std::async()
- wait()
- get()
- std::packaged_task<>
- get_future()
- std::promise 执行结果可能来自多个部分的代码
- get_future()
- set_value()
- set_exception()
- std::shared_future
- wait_for()
- wait_until()
- std::chrono::system_clock::now()
- some_clock::now()
- std::chrono::steady_clock::now()
- std::chrono::high_resolution_clock::now()
- std::chrono::duration<>
- std::chrono::time_point<>
- time_since_epoch()
- std::chrono::system_clock::to_time_point()
- std::timed_mutex and std::recursive_timed_mutex
- try_lock_for()
- try_lock_until()
- std::experimental::future
- then()
- std::experimental::when_any()
- std::experimental::when_all()
- std::experimental::latch
- count_down()
- is_ready()
- count_down_and_wait()
- std::experimental::barrier
- arrive_and_wait()
- arrive_and_drop()
- std::experimental::flex_barrier
- std::experimental::barrier
设计思想
- 生产者消费者 Code_4_1_1
- 线程安全的队列 Code_4_1_2
- 更新GUI设计的专门线程 Code_4_2_2
- 一个或多个任务被设置,一个处理任务线程
- 单个线程中处理多个连接 Code_4_2_3
- 线程有限,无法同时开启N个线程,需要若干线程,每一个线程处理多个请求时可参考
- 任务接受,处理,发送
- 条件变量进行限时等待 Code_4_3_3
- 函数式编程风格的并行快速排序 Code_4_4_1_3
- 函数式编程 与 通信式串行进程(CSP)
- 状态机 & 分离关注点 Code_4_4_2_1
- 模拟自动柜员机逻辑
- 等待-处理-转移
- 思想是设置一个状态函数指针,循环执行该指针指向函数,而每个线程处理完毕后,将下一个步骤的函数指针赋值给循环中的状态函数
- 后续连锁 登录模块设计 Code_4_4_4
- 线程执行完成,交给下一个线程完成剩余任务
- 分发收集模式、类似 map reduce Code_4_4_5
- std::experimental::when_all
- 等待所有线程执行完毕,再执行
- 并行查询 Code_4_4_6
- std::experimental::when_any()
- 发起多个线程,有一个线程完成任务即可退出
- 多线程创建数据(计数) std::experimental::latch
- 线程闩,完成线程,计数器会减少,直至为0
- 多线程复制数据 (同步组)Code_4_4_9, Code_4_4_10
- 线程卡,Vulkan中经常使用
第五章 C++ 内存模型和原子操作
5.1 内存模型基础
- 内存模型牵涉两个方面:基本结构和并发
5.1.1 对象和内存区域
- C++ 程序的数据全部都由对象构成
- C++ 标准只将“对象”定义为“某一存储范围”
- 不论对象属于什么类型,它都会存储在一个或多个内存区域中
- 用到了位域,那么请注意,它有一项重要的性质
- 尽管相邻的位域分属不同对象,但照样算作同一内存区域
- 4个要点:
- 每个变量都是对象,对象的数据成员也是对象;
- 每个对象都占用至少一块内存区域;
- 若变量属于内建基本类型(如int或char),则不论其大小,都占用一块内存区域(且仅此一块),即便它们的位置相邻或它们是数列中的元素;
- 相邻的位域属于同一内存区域。
5.1.2 对象、内存区域和并发
- 重要性质:所有与多线程相关的事项都会牵涉内存区域
- 如果两个线程各自访问分离的内存区域,则相安无事,一切运作良好;
- 反之,如果两个线程访问同一内存区域,我们就要警惕了。假使没有线程更新内存区域,则不必在意,只读数据无须保护或同步
- 重要:未定义行为是 C++ 中一种最棘手的问题。根据 C++ 标准,只要应用程序含有任何未定义行为,情况将难以预料。
- 实际开发中真的很烦
- 另一重要之处
- 凡是涉及数据竞争的内存区域,我们都通过原子操作来访问,即可避免未定义行为。
- 这种做法不能预防数据竞争本身,因为我们依旧无法指定某一原子操作,令其首先踏足目标内存区域,但此法确实使程序重回正轨,符合已定义行为的规范。
5.1.3 改动序列(modification order)
- 变量的值会随时间推移形成一个序列。在不同的线程上观察属于同一个变量的序列,如果所见各异,就说明出现了数据竞争和未定义行为
- 若我们采用了原子操作,那么编译器有责任保证必要的同步操作有效、到位
5.2 C++ 中的原子操作及其类别
- 原子操作是不可分割的操作(indivisible operation)。在系统的任一线程内,我们都不会观察到这种操作处于半完成状态;
- 非原子操作(non-atomic operation)在完成到一半的时候,有可能为另一线程所见。
- 在 C++ 环境中,多数情况下,我们需要通过原子类型实现原子操作。
5.2.1 标准原子类型
- 标准原子类型的定义位于头文件
内
- 我们可以凭借互斥保护,模拟出标准的原子类型:
- 它们全部(几乎)都具备成员函数
is_lock_free()
,准许使用者判定某一给定类型上的操作是能由原子指令(atomic instruction)直接实现
x.is_lock_free()
返回 true
- 还是要借助编译器和程序库的内部锁来实现
x.is_lock_free()
返回 false
- 原子操作的关键用途是取代需要互斥的同步方式
- 如原子操作本身也在内部使用了互斥,就很可能无法达到所期望的性能提升
- 而更好的做法是采用基于互斥的方式,该方式更加直观且不易出错
- 无锁数据结构正属于这种情况
- C++ 程序库专门为此提供了一组宏
- 它们的作用是,针对由不同整数类型特化而成的各种原子类型,在编译期判定其是否属于无锁数据结构
- 从 C++17 开始,全部原子类型都含有一个静态常量表达式成员变量
- static constexpr member variable
- X::is_always_lock_free
- 功能与上面宏相同
- 编译生成的一个特定版本的程序,当且仅当在所有支持该程序运行的硬件上,原子类型 X 全都以无锁结构形式实现,该成员变量的值才为 true
- 示例
- 假设一个程序含有原子类型
std::atomic
- 而相关的原子操作必须用到某些 CPU 指令
- 如果多种硬件可以运行该程序,但仅有其中一部分支持这些指令,那么等到运行时才可以确定它是否属于无锁结构
- 此时,
std::atomic::is_always_lock_free
的值在编译期即确定为 false
- 若在任意给定的目标硬件上,
std::atomic
都以无锁结构形式实现
std::atomic::is_always_lock_free
的值会是 true
- 宏
- 略,使用 C++ 17 特性
- 只有一个原子类型不提供
is_lock_free()
成员函数
std::atomic_flag
- 必须采取无锁操作
- 类型
std::atomic_flag
的对象在初始化时清零
- 随后即可通过成员函数
test_and_set()
查值并设置成立
- 或者由
clear()
清零
- 其余的原子类型都是通过类模板
std::atomic<>
特化得出的
- C++标准并没有为普通指针定义位运算(如&=),所以不存在专门为原子化的指针而定义的位运算。
- 开发是尽量使用
std::atomic<>
特化, 而不用别名
- 由于不具备拷贝构造函数或拷贝赋制操作符,因此按照传统做法,标准的原子类型对象无法复制,也无法赋值
- 可以接受内建类型赋值,也支持隐式地转换成内建类型,还可以直接经由成员函数处理
load()
store()
exchange()
compare_exchange_weak()
compare_exchange_strong()
- 支持复合赋值操作
+=、−=、*=
和 |=
- 整型和指针的
std::atomic<>
特化都支持 ++
和 −−
运算
- 这些操作符有对应的具名成员函数,
fetch_add()
和 fetch_or()
等
- C++ 的赋值操作符通常返回引用,指向接受赋值的对象,但原子类型的设计与此有别,要防止暗藏错误
- 多线程使用引用需要注意
- 类模板
std::atomic<>
并不局限于上述特化类型, 它其实具有泛化模板
- 可依据用户自定义类型创建原子类型的变体
- 该泛化模板所具备的操作仅限于以下几种
load()、store()
- 接受用户自定义类型的赋值,以及转换为用户自定义类型
exchange()、compare_exchange_weak()、compare_exchange_strong()
- 对于原子类型上的每一种操作,都可以提供额外的参数
- 从枚举类
std::memory_order
取值
- 用于设定所需的内存次序语义 (memory-ordering semantics)
- 枚举类
std::memory_order
具有6个可能的值
- std::memory_order_relaxed
- std::memory_order_acquire
- std::memory_order_consume
- std::memory_order_acq_rel
- std::memory_order_release
- std::memory_order_seq_cst
- 操作被划分为3类
- 存储(store)操作,可选用的内存次序有
std::memory_order_relaxed
std::memory_order_release
std::memory_order_seq_cst
- 载入(load)操作,可选用的内存次序有
std::memory_order_relaxed
std::memory_order_consume
std::memory_order_acquire
std::memory_order_seq_cs
- 读-改-写”(read-modify-write)操作,可选用的内存次序有
std::memory_order_relaxed
std::memory_order_consume
std::memory_order_acquire
std::memory_order_release
std::memory_order_acq_rel
std::memory_order_seq_cst
5.2.2 操作 std::atomic_flag
std::atomic_flag
是最简单的标准原子类型,表示一个布尔标志。
- 该类型的对象只有两种状态:成立或置零
- 唯一用途是充当构建单元,因此, 我们认为普通开发者一般不会直接使用它
std::atomic_flag
类型的对象必须由宏 ATOMIC_FLAG_INIT
初始化
- 它把标志初始化为置零状态
std::atomic_flag f=ATOMIC_FLAG_INIT
。
std::atomic_flag
- 唯一保证无锁的原子类型
- 如果
std::atomic_flag
对象具有静态存储期,它就会保证以静态方式初始化,从而避免初始化次序的问题。
- 对象在完成初始化后才会操作其标志。
std::atomic_flag
对象的初始化后,我们只能执行3种操作
- 销毁:对应析构函数
- 置零:对应
clear()
- 读取原有的值并设置标志成立:对应
test_and_set()
- 可以为
clear()
和 test_and_set()
指定内存次序
clear()
是存储操作,因此无法采用 std::memory_order_acquire
或std::memory_order_acq_rel
内存次序
test_and_set()
是“读-改-写”操作,因此能采用任何内存次序//调用显式地采用释放语义将标志清零
f.clear(std::memory_order_release);
//调用采用默认的内存次序,获取旧值并设置标志成立
bool x=f.test_and_set();
- 无法从
std::atomic_flag
对象拷贝构造出另一个对象
std::atomic_flag
功能有限,所以它可以完美扩展成自旋锁互斥(spin-lock mutex)class spinlock_mutex
{
std::atomic_flag flag;
public:
spinlock_mutex():
flag(ATOMIC_FLAG_INIT)
{}
void lock()
{
while(flag.test_and_set(std::memory_order_acquire));
}
void unlock()
{
flag.clear(std::memory_order_release);
}
};
- 上述自旋锁互斥在
lock()
函数内忙等。
- 若不希望出现任何程度的竞争,那么该互斥远非最佳选择
5.2.3 操作 std::atomic
- 相比
std::atomic_flag
,它是一个功能更齐全的布尔标志
- 无法拷贝构造或拷贝赋值
- 它们所支持的赋值操作符不返回引用,而是按值返回
store()
是存储操作
load()
是载入操作
exchange()
是“读-改-写” 操作std::atomic<bool> b;
bool x=b.load(std::memory_order_acquire);
b.store(true);
x=b.exchange(false,std::memory_order_acq_rel);
- 依据原子对象当前的值决定是否保存新值
- 引入了一种操作
- 若原子对象当前的值符合预期,就赋予新值
- 这一新操作被称为“比较-交换”(compare-exchange)
compare_exchange_weak()
和 compare_exchange_strong()
- 比较-交换函数返回布尔类型
- 如果完成了保存动作(前提是两值相等),则操作成功,函数返回 ture
- 反之操作失败,函数返回 false
- 比较-交换操作是原子类型的编程基石
- 用者给定一个期望值,原子变量将它和自身的值比较,如果相等,就存入另一既定的值
- 否则,更新期望值所属的变量,向它赋予原子变量的值
- 佯败(spurious failure)
- 原子化的比较-交换必须由一条指令单独完成,而某些处理器没有这种指令,无从保证该操作按原子化方式完成。
- 要实现比较-交换,负责的线程则须改为连续运行一系列指令,但在这些计算机上,只要出现线程数量多于处理器数量的情形,线程就有可能执行到中途因系统调度而切出,导致操作失败。这种计算机最有可能引发上述的保存失败
- compare_exchange_weak()可能佯败,所以它往往必须配合循环使用
bool expected=false;
extern atomic<bool> b; //由其他源文件的代码设定变量的值
//只要expected变量还是false,
//就说明compare_exchange_weak()的调用发生佯败,我们就继续循环
while(!b.compare_exchange_weak(expected,true) && !expected);
- 只有当原子变量的值不符合预期时,
compare_exchange_strong()
才返回 false
- 假如经过简单计算就能得出要保存的值,而在某些硬件平台上,虽然使用
compare_exchange_weak()
可能导致佯败,但改用 compare_exchange_strong()
却会形成双重嵌套循环(因 compare_exchange_strong()
自身内部含有一个循环),那么采用compare_exchange_weak()
比较有利于性能。
- 反之,如果存入的值需要耗时的计算,选择
compare_exchange_strong()
则更加合理。因为只要预期值没有变化,就可避免重复计算
- 比较-交换函数还有一个特殊之处
- 它们接收两个内存次序参数。这使程序能区分成功和失败两种情况,采用不同的内存次序语义
- 合适的做法是
- 若操作成功,就采用
std::memory_order_acq_rel
内存次序
- 否则改用
std::memory_order_relaxed
内存次序
- 两种内存次序不准用作失败操作的参数
std::atomic
和 std::atomic_flag
的另一个不同点是
- 前者有可能不具备无锁结构,它的实现可能需要在内部借用互斥,以保证操作的原子性
- 可以调用成员函数
is_lock_free()
,检查 std::atomic
是否具备真正的无锁操作
5.2.4 操作 std::atomic:算术形式的指针运算
- 它与
std::atomic
相似,同样不能拷贝复制或拷贝赋值
- 根据类模板的定义,
std::atomic
也具备成员函数
is_lock_free()
load()
store()
exchange()
compare_exchange_weak()
compare_exchange_strong()
- 但接收的参数和返回值是
T*
类型
std::atomic
提供的新操作是算术形式的指针运算
fetch_add()
- 它们返回原来的地址
- “读-改-写”操作
fetch_sub()
- 它们返回原来的地址
- “读-改-写”操作
+=
−=
++
−−
- 假设变量x属于类型
std::atomic
,其指向 Foo 对象数组中的第一项,那么操作 x+=3
会使之变为指向数组第四项,并返回 Foo*
型的普通指针class Foo{};
Foo some_array[5];
std::atomic<Foo*> p(some_array);
//接收附加的参数
//p.fetch_add(3,std::memory_order_release);
Foo* x=p.fetch_add(2); //令p加2,返回旧值
assert(x==some_array);
assert(p.load()==&some_array[2]);
x=(p-=1); // 令p减1,返回新值
assert(x==&some_array[1]);
assert(p.load()==&some_array[1]);
5.2.5 操作标准整数原子类型
- 在
std::atomic
和 std::atomic
这样的整数原子类型上,可以执行的操作颇为齐全
- 常用的原子操作
load()
store()
exchange()
compare_exchange_weak()
compare_exchange_strong()
- 原子运算
fetch_add()
fetch_sub()
fetch_and()
fetch_or()
fetch_xor()
- 复合赋值形式
+=
、−=
、&=
、|=
和 ^=
- 前后缀形式的自增和自减
++x
、x++
、−−x
和 x−−
5.2.6 泛化的 std::atomic<> 类模板
- 要满足一定条件才能使用模板
std::atomic<>
- 必须具备平实拷贝赋值操作符(trivial copy-assignment operator),
- 它不得含有任何虚函数,也不可以从虚基类派生得出
- 还必须由编译器代其隐式生成拷贝赋值操作符
- 若自定义类型具有基类或非静态数据成员,则它们同样必须具备平实拷贝赋值操作符
- 由于以上限制,赋值操作不涉及任何用户编写的代码,因此编译器可借用
memcpy()
或采取与之等效的行为完成它
- 编译器往往没能力为
std::atomic<>
生成无锁代码
- 因此, 它必须在内部运用锁保护所有操作
- 内建浮点类型确实满足了
memcpy()
和 memcmp()
的适用条件
- 故类型
std::atomic
和 std::atomic
可以为我们所用
- 若在这两个原子类型上调用
compare_exchange_strong()
函数,其行为可能出人意料
- 相比
int
或 void*
的体积,只要用户自定义的 UDT 类型的体积不超过它们,那么在大多数常见的硬件平台上,都可以为 std::atomic
类型采用原子指令
- 在某些硬件平台上,就算自定义类型的体积是int或void*的两倍,同样会得到其原子指令的支持
- 这些硬件平台往往都具有名为“双字比较-交换”的指令
- Double-Word-Compare-And-Swap,DWCAS
- 它与compare_exchange_xxx()函数对应
- 这种硬件支持有助于编写无锁代码
- 原子对象无法被创建
std::atomic>
- 原因是
vector
含有非平实拷贝构造函数和非平实拷贝赋值操作符
- 各原子类型上可执行的操作
- 可查询该节的表
5.2.7 原子操作的非成员函数
- 非成员函数,与各原子类型上的所有操作逐一等价
- 大部分非成员函数依据对应的成员函数命名,只不过冠以前缀 “atomic_”
- 如
std::atomic_load()
- 带有后缀 “_explicit”,接收更多参数以指定内存次序,
- 不带后缀不接收内存次序参数
- 这些非成员函数要兼容C语言,所以它们全都只接收指针,而非引用
- 操作
std::atomic_flag
的非成员函数是
std::atomic_flag_test_and_set()
std::atomic_flag_clear()
- 它们也具有其他变体,以后缀“_explicit”结尾
std::atomic_flag_test_and_set_explicit()
std::atomic_flag_clear_explicit()
- C++ 标准委员会认为,额外提供std:shared_ptr<>的原子函数十分重要,所以标准库给出了共享指针的原子操作
- 载入、存储、交换和比较-交换
std::shared_ptr<my_data> p;
void process_global_data()
{
std::shared_ptr<my_data> local=std::atomic_load(&p);
process_data(local);
}
void update_global_data()
{
std::shared_ptr<my_data> local(new my_data);
std::atomic_store(&p,local);
}
- 操作
std::share_ptr
的函数也具有变体
- 并发技术规约还提供了
std::experimental::atomic_shared_ptr
,其也是一种原子类型。我们须包含头文件
才能使用该类型
- 与std::atomic上的操作一样, 它具备以下操作
- 载入、存储、交换和比较-交换
atomic_shared_ptr<>
被设计成一个独立类型,
- 因为按照这种形式,它有机会通过无锁方式实现,而且比起普通的
std::shared_ptr
对象,它没有增加额外开销
- 但是在目标硬件平台上,我们仍需查验它是否属于无锁实现
is_lock_free()
判定
- 在多线程环境下处理共享指针,我们要避免采用普通的
std::share_ptr
类型
- 也不要通过非成员原子函数对其进行操作
- 类型
std::experimental::atomic_shared_ptr
应予优先采用(就算它不是无锁实现)
- 可以使代码更加清晰,并确保全部访问都按原子化方式进行
- 还能预防误用普通函数,最终避免数据竞争。
- 标准原子类型不仅能避免未定义操作、防范数据竞争,还能让用户强制线程间的操作服从特定次序
std::mutex
和 std::future<>
,都以这种强制服从的内存次序为基础
- 其中奥义
- 内存模型中涉及的并发细节,以及运用原子操作同步数据和强制施行内存次序
5.3 同步操作和强制次序
- 假设有两个线程共同操作一个数据结构,其中一个负责增添数据,另一个负责读取数据。为了避免恶性条件竞争,写线程设置一个标志,用以表示数据已经存储妥当,而读线程则一直待命,等到标志成立才着手读取
- 原子变量
data_ready
的操作提供了所需的强制次序,它属于 std::atomic
类型#include
#include
#include
std::vector<int> data;
std::atomic<bool> data_ready(false);
void reader_thread()
{
while(!data_ready.load())
{
std::this_thread::sleep(std::chrono::milliseconds(1));
}
std::cout<<"The answer="<<data[0]<<"\n";
}
void writer_thread()
{
data.push_back(42);
data_ready=true;
}
5.3.1 同步关系
- 同步关系的基本思想是:对变量x执行原子写操作W和原子读操作R,且两者都有适当的标记。只要满足下面其中一点,它们即彼此同步
- R读取了W直接存入的值。
- W所属线程随后还执行了另一原子写操作,R读取了后面存入的值
- 任意线程执行一连串“读-改-写”操作(如fetch_add()或compare_exchange_weak()),而其中第一个操作读取的值由W写出
- 用白话说就是:无论什么操作,都要操作W写后的数据
5.3.2 先行关系
- 先行关系和严格先行关系是在程序中确立操作次序的基本要素
- 它们的用途是清楚界定哪些操作能看见其他哪些操作产生的结果
- 但如果同一语句内出现多个操作,则它们之间通常不存在先行关系
- 因为C++标准没有规定执行次序
- 先行关系和严格先行关系的不同
- 在线程间先行关系和先行关系中,各种操作都被标记为
memory_order_consume
- 严格先行关系则无此标记
- 由于绝大多数代码都不会用
memory_order_consume
标记,因此实际上这一区别对我们影响甚微。
- 简洁起见,本书后文将一律使用“先行关系”
5.3.3 原子操作的内存次序
- 原子类型上的操作服从 6 种内存次序:
- memory_order_relaxed
- memory_order_consume
- memory_order_acquire
- memory_order_release
- memory_order_acq_rel
- memory_order_seq_cst。
- 是可选的最严格的内存次序
- 虽然内存次序共有6种,但它们只代表3种模式:
- 先后一致次序
- memory_order_seq_cst
- 获取-释放次序
- memory_order_consume
- memory_order_acquire
- memory_order_release
- memory_order_acq_rel
- 宽松次序
- memory_order_relaxed
- 在不同的CPU架构上,这几种内存模型也许会有不同的运行开销。
- 采用x86或x86-64架构的CPU(如在台式计算机中常见的Intel和AMD处理器)并不需要任何额外的同步指令,就能确保服从获取-释放次序的操作的原子化,甚至不采取特殊的载入操作就能保障先后一致次序,而且其存储行为的开销仅略微增加
- C++ 提供了上述各种内存序模型,资深程序员可以自由选用,籍此充分利用更为细分的次序关系,从而提升程序性能
- 普通开发者则能采取默认方式,按先后一致性次序执行原子操作
- 比起其他内存序,它分析起来要容易很多
先后一致次序 memory_order_seq_cst
- 事件视为按先后顺序发生,其操作与这种次序保持一致
- 尽管这种内存次序易于理解,但代价无可避免
- 在弱保序的多处理器计算机上,保序操作会导致严重的性能损失
- 若想使用这种内存次序,而又在意它对性能的影响,则应查阅目标处理器架构的说明文档
- 先后一致次序示例
- Code_5_3_3_1
- 先后一致次序是最直观、最符合直觉的内存次序
- 但由于它要求在所有线程间进行全局同步,因此也是代价最高的内存次序。
非先后一致次序
- 线程之间不必就事件发生次序达成一致。
- 我们不仅须舍弃交替执行完整操作的思维模式
- 还得修正原来的认知,不再任由编译器或处理器自行重新排列指令
宽松次序
理解宽松次序
- 强烈建议避免使用宽松原子操作,
获取-释放次序
- 避免了“绝对先后一致次序”的额外开销
- 获取-释放次序比宽松次序严格一些
- 原子化载入即为获取操作(memory_order_acquire)
- 原子化存储即为释放操作(memory_order_release)
- 而原子化“读-改-写”操作则为获取或释放操作,或二者皆是(memory_order_acq_rel)
- 像
fetch_add()
和 exchange()
- 这种内存次序在成对的读写线程之间起到同步作用
- 释放与获取操作构成同步关系
- 获取-释放次序可用于多线程之间的数据同步,
- 即使“过渡线程”的操作不涉及目标数据,也照样可行
- Code_5_3_3_2
- 注意,要理解代码,需要结合书和书中所有相关示例去看
通过获取-释放次序传递同步
- 如果原子操作对先后一致的要求不是很严格,那么由成对的获取-释放操作实现同步,开销会远低于由保序操作实现的全局一致顺序
- Code_5_3_3_3
获取-释放次序和 memory_order_consume 次序造成的数据依赖
- memory_order_consume 次序是获取-释放次序的组成部分
- 相当特别
- 它完全针对数据依赖,引入了线程间先行关系中的数据依赖细节
- C++17 标准明确建议对其不予采用
- 不应在代码中使用memory_order_consume次序
小节
- 这节虽然看懂了,但是还是有些迷茫
- 后期关注 先后一致次序 和 获取-释放次序
- 前者适合新手,但是开销较大
- 后者性能会更好,但更烧脑
5.3.4 释放序列和同步关系
- 构成一个释放序列
- 如果存储操作的标记是
- memory_order_release
- memory_order_acq_rel
- memory_order_seq_cst
- 而载入操作则以
- memory_order_consume
- memory_order_acquire
- memory_order_seq_cst
- 这些操作前后相扣成链,每次载入的值都源自前面的存储操作,那么该操作链由一个释放序列组成
- 若最后的载入操作服从内存次序为 memory_order_acquire 或 memory_order_seq_cst 则最初的存储操作与它构成同步关系
- 共享队列容器操作 生产者、消费者
- Code_5_3_4
5.3.5 栅栏
- 栅栏也常常被称作“内存卡”或“内存屏障”
- 它们在代码中划出界线,限定某些操作不得通行
- 用途是强制施加内存次序,却无须改动任何数据
- 通常,它们与服从 memory_order_relaxed 次序的原子操作组合使用
- 栅栏可以令宽松操作服从一定的次序
- Code_5_3_5
- 栅栏的整体运作思路是:
- 若存储操作处于释放栅栏后面,而存储操作的结果为获取操作所见,则该释放栅栏与获取操作同步
- 若载入操作处于获取栅栏前面,而载入操作见到了释放操作的结果,则该获取栅栏与释放操作同步
- 栅栏存在与否并不影响已经加诸其上的次序
5.3.6 凭借原子操作令非原子操作服从内存次序
- Code_5_3_6
- 与 Code_5_3_5 差距就是,使用普通 bool 类型 而不是原子类型
- 令非原子操作服从内存次序的不只有栅栏。
- 凭借分别标记为 memory_order_release 和 memory_order_consume 的原子操作,就能按非原子化的方式访问动态分配的对象
5.3.7 强制非原子操作服从内存次序
- 先行关系中蕴含着控制流程的先后执行顺序
- 利用这一重要性质,即可借原子操作强制非原子操作服从内存次序
- 第2~4章讲解了多种同步机制,根据同步关系,按各种形式为相关内存次序提供了保证
- std::thread
- 执行了join调用,而此函数成功返回,则该线程的运行完成与这一返回动作同步
- std::mutex、std::timed_mutex、std::recursive_mutex和std::recursive_timed_mutex
- 给定一互斥对象,在其加锁和解锁的操作序列中,每个unlock()调用都与下一个lock()调用同步,
- 或与下一个try_lock()、try_lock_for()、try_lock_until()的成功调用同步
- 如果try_lock()、try_lock_for()或try_lock_until()的调用失败,则不构成任何同步关系
- std::shared_mutex和std::shared_timed_mutex
- 其加锁和解锁的操作序列中,每个unlock()调用都与下一个 lock()调用同步,或与下一个 try_lock()、try_lock_for()、try_lock_until()、try_lock_shared()、try_lock_shared_for()和try_lock_shared_until()的成功调用同步
- std::promise、std::future和std::shared_future
- future对象上调用wait()、get()、wait_for()或wait_until(),成功返回std::future_status::ready,那么这两次调用的成功返回构成同步
- 我们在关联的std::future对象上调用wait()、get()、wait_for()或wait_until(),成功返回std::future_status:: ready,那么std::promise对象的析构函数与该成功返回构成同步。
- std::packaged_task、std::future和std::shared_future
- 给定一std::packaged_task对象,则我们由get_future()得到关联的std::future对象,它们共享异步状态。若包装的任务由std::packaged_task的函数调用操作符运行,我们在关联的std::future对象上调用wait()、get()、wait_for()或wait_until(),成功返回std::future_status::ready,那么任务的运行完结与该成功返回构成同步
- std::async、std::future和std::shared_futur
- 我们在该 std::future 对象上调用wait()、get()、wait_for()或wait_until(),成功返回std::future_status:: ready,那么任务线程的完成与该成功返回构成同步。
- std::experimental::future、std::experimental::shared_future和后续函数
- 异步共享状态会因目标事件触发而变成就绪,共享状态上所编排的后续函数也随之运行,该事件与后续函数的启动构成同步
- std::experimental::latch
- 给定一std::experimental::latch实例,若在其上调用count_down()或count_down_and_wait(),则每次调用的启动都与其自身的完成同步。
- std::experimental::barrie
- 给定一std::experimental::barrier实例,若在其上调用arrive_and_wait()或arrive_and_drop(),则每次调用的启动都与下一次arrive_and_wait()的运行完成同步。
- std::experimental::flex_barrie
- 给定一std::experimental::flex_barrier实例,若在其上调用arrive_and_wait()或arrive_and_drop(),则每次调用的启动都与下一次arrive_and_wait()的运行完成同步
- 给定一std::experimental::flex_barrier实例,若在其上调用arrive_and_wait()或arrive_and_drop(),则每次调用的启动都与其补全函数的下一次启动同步
- 给定一std::experimental::flex_barrier实例,若在其上调用arrive_and_wait()或arrive_and_drop(),则这些调用会因等待补全函数的完成而发生阻塞,而补全函数的返回与这些调用的完成构成同步
- std::condition_variable和std::condition_variable_any
- 条件变量并不提供任何同步关系。
- 它们本质上是忙等循环的优化,其所有同步功能都由关联的互斥提供
总结
关键 API
- is_lock_free()
- is_always_lock_free()
- std::atomic_flag
- test_and_set()
- clear()
- std::atomic<>
- load()
- store()
- exchange()
- compare_exchange_weak()
- compare_exchange_strong()
- std::memory_order
- std::memory_order_relaxed
- std::memory_order_acquire
- std::memory_order_consume
- std::memory_order_acq_rel
- std::memory_order_release
- std::memory_order_seq_cst
- std::atomic_flag
- std::atomic
- store()
- load()
- exchange()
- std::atomic
- is_lock_free()
- load()
- store()
- exchange()
- compare_exchange_weak()
- compare_exchange_strong()
- fetch_add()
- fetch_sub()
- std::atomic 和 std::atomic
- load()
- store()
- exchange()
- compare_exchange_weak()
- compare_exchange_strong()
- std::atomic<>
- std::atomic_load()
- std::atomic_flag_test_and_set()
- std::atomic_flag_clear()
- std::atomic_flag_test_and_set_explicit()
- std::atomic_flag_clear_explicit()
- std::experimental::atomic_shared_ptr
设计思想
- 自旋锁互斥 (笔记中)
- 共享队列容器操作 生产者、消费者 Code_5_3_4
你可能感兴趣的:(笔记,c++)