元数据
C++并发编程实战(第2版)
- 书名: C++并发编程实战(第2版)
- 作者: 安东尼·威廉姆斯
- 简介: 这是一本介绍C++并发和多线程编程的深度指南。本书从C++标准程序库的各种工具讲起,介绍线程管控、在线程间共享数据、并发操作的同步、C++内存模型和原子操作等内容。同时,本书还介绍基于锁的并发数据结构、无锁数据结构、并发代码,以及高级线程管理、并行算法函数、多线程应用的测试和除错。本书还通过附录及线上资源提供丰富的补充资料,以帮助读者更完整、细致地掌握C++并发编程的知识脉络。 本书适合需要深入了解C++多线程开发的读者,以及使用C++进行各类软件开发的开发人员、测试人员,还可以作为C++线程库的参考工具书。
- 出版时间 2021-12-01 00:00:00
- ISBN: 9787115573551
- 分类: 计算机-编程设计
- 出版社: 人民邮电出版社
高亮划线
3.1 线程间共享数据的问题
- 📌 采取专门措施保护不变量 ^42568675-44-1322-1333
- ⏱ 2023-07-30 10:15:58
3.1.1 条件竞争
-
📌 在并发编程中,操作由两个或多个线程负责,它们争先让线程执行各自的操作,而结果取决于它们执行的相对次序,所有这种情况都是条件竞争。 ^42568675-45-563-627
- ⏱ 2023-07-30 10:21:41
-
📌 当论及并发时,条件竞争通常特指恶性条件竞争。 ^42568675-45-747-769
- ⏱ 2023-07-30 10:21:48
-
📌 “数据竞争”(data race):并发改动单个对象而形成的特定的条件竞争 ^42568675-45-800-837
- ⏱ 2023-07-30 10:22:11
-
📌 诱发恶性条件竞争的典型场景是,要完成一项操作,却需改动两份或多份不同的数据,如上例中的两个链接指针。因为操作涉及两份独立的数据,而它们只能用单独的指令改动,当其中一份数据完成改动时,别的线程有可能不期而访。 ^42568675-45-895-998
- ⏱ 2023-07-30 10:24:33
3.1.2 防止恶性条件竞争
-
📌 我们有几种方法防止恶性条件竞争。最简单的就是采取保护措施包装数据结构,确保不变量被破坏时,中间状态只对执行改动的线程可见。在其他访问同一数据结构的线程的视角中,这种改动要么尚未开始,要么已经完成。 ^42568675-46-373-471
- ⏱ 2023-07-30 10:29:59
-
📌 另一种方法是,修改数据结构的设计及其不变量,由一连串不可拆分的改动完成数据变更,每个改动都维持不变量不被破坏。这通常被称为无锁编程 ^42568675-46-528-593
- ⏱ 2023-07-30 10:29:13
-
📌 还有一种防止恶性条件竞争的方法,将修改数据结构当作事务(transaction)来处理,类似于数据库在一个事务内完成更新:把需要执行的数据读写操作视为一个完整序列,先用事务日志存储记录,再把序列当成单一步骤提交运行。若别的线程改动了数据而令提交无法完整执行,则事务重新开始。 ^42568675-46-709-846
- ⏱ 2023-07-30 10:28:59
3.2.1 在C++中使用互斥
- 📌 如果成员函数返回指针或引用,指向受保护的共享数据,那么即便成员函数全都按良好、有序的方式锁定互斥,仍然无济于事,因为保护已被打破,出现了大漏洞。 ^42568675-48-2213-2285
- ⏱ 2023-07-30 10:37:14
3.2.4 死锁:问题和解决方法
-
📌 防范死锁的建议通常是,始终按相同顺序对两个互斥加锁。 ^42568675-51-831-857
- ⏱ 2023-07-30 11:02:42
-
📌 若加锁操作涉及多个互斥,则std::lock()函数的语义是“全员共同成败”(all-or-nothing,或全部成功锁定,或没获取任何锁并抛出异常) ^42568675-51-2647-2722
- ⏱ 2023-07-30 11:06:00
-
📌 std:: scoped_lock<>和std::lock_guard<>完全等价,只不过前者是可变参数模板(variadic template) ^42568675-51-2805-2890
- ⏱ 2023-07-30 11:06:37
3.2.5 防范死锁的补充准则
-
📌 若要遍历链表,线程必须持有当前节点的锁,同时在后续节点上获取锁,从而确保前向指针不被改动。一旦获取了后续节点上的锁,当前节点的锁便再无必要,遂可释放。上述加锁方式很像步行过程中双腿交替迈进。 ^42568675-52-1838-1962
- ⏱ 2023-07-30 11:17:13
-
📌 此处有一个方法可防范死锁:规定遍历的方向。从而令线程总是必须先锁住A,再锁住B,最后锁住C。这样,我们就可以以禁止逆向遍历为代价而防范可能的死锁。 ^42568675-52-2597-2670
- ⏱ 2023-07-30 11:19:21
3.3.1 在初始化过程中保护共享数据
- 📌 std::once_flag类和std:: call_once()函数 ^42568675-57-2434-2469
- ⏱ 2023-07-30 12:00:56
3.3.2 保护甚少更新的数据结构
-
📌 更新操作可用std::lock_guardstd::shared_mutex和std::unique_lockstd::shared_mutex锁定 ^42568675-58-1483-1573
- ⏱ 2023-07-30 12:04:41
-
📌 对于那些无须更新数据结构的线程,可以另行改用共享锁std::shared_lockstd::shared_mutex ^42568675-58-1620-1683
- ⏱ 2023-07-30 12:05:00
3.3.3 递归加锁
-
📌 std::recursive_mutex ^42568675-59-461-481
- ⏱ 2023-07-30 12:06:40
-
📌 我们通常可以采取更好的方法:根据这两个公有函数的共同部分,提取出一个新的私有函数,新函数由这两个公有函数调用,而它假定互斥已经被锁住,遂无须重复加锁 ^42568675-59-1106-1180
- ⏱ 2023-07-30 12:08:40
4.1.1 凭借条件变量等待条件成立
-
📌 若成立(lambda函数返回true),则wait()返回;否则(lambda函数返回false),wait()解锁互斥,并令线程进入阻塞状态或等待状态。线程乙将数据准备好后,即调用notify_one()通知条件变量,线程甲随之从休眠中觉醒(阻塞解除),重新在互斥上获取锁,再次查验条件:若条件成立,则从wait()函数返回,而互斥仍被锁住;若条件不成立,则线程甲解锁互斥,并继续等待。 ^42568675-63-2703-2897
- ⏱ 2023-07-30 19:56:12
-
📌 如果线程甲重新获得互斥,并且查验条件,而这一行为却不是直接响应线程乙的通知,则称之为伪唤醒(spuriouswake)。按照C++标准的规定,这种伪唤醒出现的数量和频率都不确定。故此,若判定函数有副作用[4],则不建议选取它来查验条件。 ^42568675-63-3350-3558
- ⏱ 2023-07-30 19:59:38
4.2 使用future等待一次性事件发生
-
📌 C++标准程序库使用future来模拟这类一次性事件 ^42568675-65-555-581
- ⏱ 2023-07-30 20:10:12
-
📌 一旦目标事件发生,其future即进入就绪状态,无法重置。 ^42568675-65-794-823
- ⏱ 2023-07-30 20:10:17
4.2.1 从后台任务返回值
- 📌 我们从std::async()函数处获得std::future对象(而非std::thread对象),运行的函数一旦完成,其返回值就由该对象最后持有。若要用到这个值,只需在future对象上调用get(),当前线程就会阻塞,以便future准备妥当并返回该值。 ^42568675-66-770-900
- ⏱ 2023-07-30 20:16:18
4.2.2 关联future实例和任务
- 📌 std::packaged_task<>是类模板,其模板参数是函数签名(function signature) ^42568675-67-761-822
- ⏱ 2023-07-30 20:23:27
4.2.3 创建std::promise
- 📌 配对的std::promise和std::future可实现下面的工作机制:等待数据的线程在future上阻塞,而提供数据的线程利用相配的promise设定关联的值,使future准备就绪。 ^42568675-68-886-981
- ⏱ 2023-07-30 20:39:13
4.2.4 将异常保存到future中
- 📌 若经由std::async()调用的函数抛出异常,则会被保存到future中,代替本该设定的值,future随之进入就绪状态,等到其成员函数get()被调用,存储在内的异常即被重新抛出 ^42568675-69-1009-1101
- ⏱ 2023-07-30 20:37:38
4.2.5 多个线程一起等待
- 📌 std::future具有成员函数share(),直接创建新的std::shared_future对象,并向它转移归属权。 ^42568675-70-2870-2931
- ⏱ 2023-07-30 20:45:02
4.3 限时等待
- 📌 有两种超时(timeout)机制可供选用:一是迟延超时(duration-based timeout),线程根据指定的时长而继续等待(如30毫秒);二是绝对超时(absolute timeout),在某特定时间点(time point)来临之前,线程一直等待。大部分等待函数都具有变体,专门处理这两种机制的超时。处理迟延超时的函数变体以“_for”为后缀,而处理绝对超时的函数变体以“_until”为后缀。 ^42568675-71-545-748
- ⏱ 2023-07-30 20:46:25
4.4.1 利用future进行函数式编程
- 📌 通过std::async()在另一线程上操作 ^42568675-77-5436-5458
- ⏱ 2023-07-30 23:09:23
4.4.3 符合并发技术规约的后续风格并发[30]
-
📌 给定future对象fut,调用then(continuation)即可为之增添后续函数continuation()。 ^42568675-79-931-990
- ⏱ 2023-07-30 22:17:36
-
📌 我们无法向后续函数传递参数,因为参数已经由程序库预设好,先前准备就绪的future会传入后续函数 ^42568675-79-1736-1784
- ⏱ 2023-07-30 22:20:44
4.4.9 基本的线程卡类std::experimental::barrier
-
📌 假定有一组线程在协同处理某些数据,各线程相互独立,分别处理数据,因此操作过程不必同步。但是,只有在全部线程都完成各自的处理后,才可以操作下一项数据或开始后续处理,std::experimental::barrier针对的就是这种场景。 ^42568675-85-575-692
- ⏱ 2023-07-30 22:44:33
-
📌 线程闩的意义在于关闸拦截:一旦它进入了就绪状态,就始终保持不变。而线程卡则不同,线程卡会释放等待的线程并且自我重置,因此它们可重复使用。另外,线程卡只与一组固定的线程同步,若某线程不属于同步组,它就不会被阻拦,亦无须等待相关的线程卡变为就绪。只要在线程卡上调用arrive_and_drop(),即可令线程显式地脱离其同步组,那样,它就再也无法被阻拦,因此也不能等待线程卡进入就绪状态,并且,在下一个同步周期中,必须运行到该线程卡处的线程数目(阻拦数目)要比当前周期少1。 ^42568675-85-915-1151
- ⏱ 2023-07-30 22:37:24
4.4.10 std::experimental::flex_barrier——std::experimental::barrier的灵活版本
-
📌 std::experimental::flex_barrier类的接口与std::experimental::barrier类的不同之处仅仅在于:前者具备另一个构造函数,其参数既接收线程的数目,还接收补全函数(completion function)。只要全部线程都运行到线程卡处,该函数就会在其中一个线程上运行(并且是唯一一个)。它不但提供了机制,可以设定后续代码,令其必须按串行方式运行,还给出了方法,用于改变下一同步周期须到达该处的线程数目(所阻拦的线程数目)。 ^42568675-86-430-664
- ⏱ 2023-07-30 22:54:10
-
📌 若返回值是-1⑤,就说明参与同步的线程保持数目不变;若返回值是0或正数⑤,则将其作为指标,设定参与下一同步周期的线程数目。 ^42568675-86-2459-2520
- ⏱ 2023-07-30 22:52:25
5.2.2 操作std::atomic_flag
- 📌 在拷贝构造或拷贝赋值的过程中,必须先从来源对象读取值,再将其写出到目标对象。这是在两个独立对象上的两个独立操作,其组合不可能是原子化的。所以,原子对象禁止拷贝赋值和拷贝构造。 ^42568675-95-1723-1810
- ⏱ 2023-07-28 01:57:50
5.2.3 操作std::atomic
-
📌 原子类型的又一个常见模式:它们所支持的赋值操作符不返回引用,而是按值返回(该值属于对应的非原子类型)。 ^42568675-96-782-833
- ⏱ 2023-07-28 02:32:50
-
📌 比较-交换操作是原子类型的编程基石。使用者给定一个期望值,原子变量将它和自身的值比较,如果相等,就存入另一既定的值;否则,更新期望值所属的变量,向它赋予原子变量的值。 ^42568675-96-1831-1914
- ⏱ 2023-07-28 02:42:28
-
📌 原子化的比较-交换必须由一条指令单独完成,而某些处理器没有这种指令,无从保证该操作按原子化方式完成。要实现比较-交换,负责的线程则须改为连续运行一系列指令,但在这些计算机上,只要出现线程数量多于处理器数量的情形,线程就有可能执行到中途因系统调度而切出,导致操作失败。这种计算机最有可能引发上述的保存失败,我们称之为佯败(spurious failure)。其败因不是变量值本身存在问题,而是函数执行时机不对。因为compare_exchange_weak()可能佯败,所以它往往必须配合循环使用。 ^42568675-96-2135-2383
- ⏱ 2023-07-29 13:07:47
-
📌 如果没有设定失败操作的内存次序,那么编译器就假定它和成功操作具有同样的内存次序,但其中的释放语义会被移除(therelease part of the ordering):memory_order_release会变成std::memory_order_relaxed,而std::memory_order_acq_rel则变成std::memory_order_acquire。若成功和失败两种情况都未设定内存次序,则它们采用默认内存次序std::memory_order_seq_cst ^42568675-96-3738-3984
- ⏱ 2023-07-28 09:52:12
5.3 同步操作和强制次序
- 📌 在一份数据上进行非原子化的读②与写③,却没有强制这些访问服从一定的次序,将导致未定义行为 ^42568675-101-1126-1170
- ⏱ 2023-07-28 10:38:48
5.3.3 原子操作的内存次序
-
📌 默认内存次序之所以命名为“先后一致次序” ^42568675-104-1530-1550
- ⏱ 2023-07-28 11:17:37
-
📌 要保持绝对先后一致,所有线程都必须采用保序原子操作。 ^42568675-104-2248-2274
- ⏱ 2023-07-28 11:50:37
-
📌 图5.3 先后一致操作与先行关系 ^42568675-104-4675-4691
- ⏱ 2023-07-28 15:45:43
-
📌 变量x和y分别执行操作,让各自的值发生变化,但它们是两个不同的原子变量,因此宽松次序不保证其变化可为对方所见。 ^42568675-104-7035-7090
- ⏱ 2023-07-28 15:54:55
-
📌 “告诉我列表最后的值,再记录一个新值”(交换操作) ^42568675-104-12852-12877
- ⏱ 2023-07-28 18:52:36
-
📌 “若我给出的值与列表最后的值相等,就写下这个值;否则,请告诉我最后的值是什么”(compare_exchange_strong()函数) ^42568675-104-12881-12949
- ⏱ 2023-07-28 18:52:52
-
📌 图5.6 获取-释放操作及其先行关系 ^42568675-104-15353-15371
- ⏱ 2023-07-28 16:52:36
-
📌 若将获取-释放操作与保序操作交错混杂,那么保序载入的行为就与服从获取语义的载入相同,保序存储的行为则与服从释放语义的存储相同。如果“读-改-写”操作采用保序语义,则其行为是获取和释放的结合。混杂其间的宽松操作仍旧宽松,但由于获取-释放语义引入了同步关系(也附带引入了先行关系),这些操作的宽松程度因此受到限制。 ^42568675-104-21027-21182
- ⏱ 2023-07-28 23:15:56
-
📌 如果要凭借互斥保护数据,由于锁具有排他性质,因此其上的加锁和解锁行为服从先后一致次序,就如同我们明令强制它们采用保序语义一样 ^42568675-104-21353-21415
- ⏱ 2023-07-28 23:26:17
5.3.5 栅栏
-
📌 栅栏(fence) ^42568675-106-371-380
- ⏱ 2023-07-29 00:27:37
-
📌 栅栏也常常被称作“内存卡”或“内存屏障” ^42568675-106-513-533
- ⏱ 2023-07-29 00:27:11
5.3.7 强制非原子操作服从内存次序
- 📌 给定一互斥对象,在其加锁和解锁的操作序列中,每个unlock()调用都与下一个lock()调用同步 ^42568675-108-2055-2104
- ⏱ 2023-07-29 00:43:22
6.2.3 采用精细粒度的锁和条件变量实现线程安全的队列容器
- 📌 为了采取精细粒度的锁操作,我们需要深入队列的实现,分析其组成,为不同的数据单独使用互斥。 ^42568675-116-449-493
- ⏱ 2023-07-29 06:35:16
6.3.2 采用多种锁编写线程安全的链表
- 📌 链表若要具备精细粒度的锁操作,则基本思想是让每个节点都具备自己的互斥。 ^42568675-119-1443-1478
- ⏱ 2023-07-29 08:17:57
7.1 定义和推论
- 📌 操作系统往往会把被阻塞的线程彻底暂停,并将其时间片分配给其他线程,等到有线程执行了恰当的操作,阻塞方被解除。恰当的操作可能是释放互斥、知会条件变量,或是为future对象装填结果值而令其就绪。 ^42568675-122-511-607
- ⏱ 2023-07-29 09:25:07
7.1.1 非阻塞型数据结构
- 📌 无阻碍(obstruction-free):假定其他线程全都暂停,则目标线程将在有限步骤内完成自己的操作。无锁(lock-free):如果多个线程共同操作同一份数据,那么在有限步骤内,其中某一线程能够完成自己的操作。免等(wait-free):在某份数据上,每个线程经过有限步骤就能完成自己的操作,即便该份数据同时被其他多个线程所操作。 ^42568675-123-1139-1367
- ⏱ 2023-07-29 12:05:45
7.1.2 无锁数据结构
-
📌 如果某数据结构具备无锁特性,那么它必须能够同时接受多个线程的访问,但多个线程所执行的操作不一定相同:无锁队列准许一个线程压入数据,另一线程则同时弹出数据,但若两个线程同时压入新数据,却有可能导致出错。不仅如此,假设某线程访问该数据结构,操作系统的调度器却在操作到中途时令其停止,那么其他线程必须依然能分别完成自己的操作,而不必等待暂停的线程。 ^42568675-124-371-542
- ⏱ 2023-07-29 12:12:02
-
📌 一旦在算法中对数据结构执行比较-交换操作,其中就通常会含有循环。在当前线程更新目标数据结构的过程中,别的线程有可能同时改动了同一数据结构,故需借比较-交换操作来判定这种情况是否出现。若出现,则当前线程需重新执行部分操作,并再次进行比较-交换操作。 ^42568675-124-571-694
- ⏱ 2023-07-29 12:18:59
7.1.4 无锁数据结构的优点和缺点
-
📌 本质上,采用无锁数据结构的首要原因是:最大限度地实现并发。 ^42568675-126-377-406
- ⏱ 2023-07-29 12:29:50
-
📌 互斥锁的根本意图就是杜绝并发功能。 ^42568675-126-456-473
- ⏱ 2023-07-29 12:32:32
-
📌 由于无锁数据结构完全不含锁,因此不可能出现死锁,但活锁(live lock)反而有机会出现。假设两个线程同时更改同一份数据结构,若它们所做的改动都导致对方从头开始操作,那双方就会反复循环,不断重试,这种现象即为活锁。 ^42568675-126-1002-1110
- ⏱ 2023-07-29 12:38:11
7.2.1 实现线程安全的无锁栈[2]
-
📌 步骤3改用原子化的比较-交换操作,使head指针由步骤2读出后就不再改动。万一发生改动,我们就循环重试。 ^42568675-128-1202-1254
- ⏱ 2023-07-29 12:54:17
-
📌 我们在④处运用compare_exchange_weak()做判断,确定head指针与new_node→next所存储的值③是否依然相同,若相同,就将head指针改为指向new_node。这行代码充分利用了比较-交换函数的功能,甚为精妙:若它返回false,则表明对比的两个指针互异(head指针被其他线程改动过),第一个参数new_node→next就被更新成head指针的当前值。 ^42568675-128-2015-2207
- ⏱ 2023-07-29 13:02:55
7.2.2 制止麻烦的内存泄漏:在无锁数据结构中管理内存
-
📌 我们需要针对节点实现特定用途的垃圾回收器 ^42568675-129-923-943
- ⏱ 2023-07-29 23:27:40
-
📌 仅仅跟踪pop()函数访问过的节点 ^42568675-129-987-1004
- ⏱ 2023-07-29 23:27:48
-
📌 我们需要维护一个“等待删除链表”(简称“候删链表”),每次执行弹出操作都向它加入相关节点,等到没有线程调用pop()时,才删除候删链表中的节点。如何得知目前没有线程调用pop()?答案很简单,即对调用进行计数。如果为pop()函数设置一个计数器,使之在进入函数时自增,在离开函数时自减,那么,当计数变为0时,我们就能安全删除候删链表中的节点。该计数器必须原子化,才可以安全地接受多线程访问。 ^42568675-129-1128-1323
- ⏱ 2023-07-29 23:30:33
7.2.3 运用风险指针检测无法回收的节点
- 📌 假使当前线程要访问某对象,而它却即将被别的线程删除,那就让前者设置一指涉目标对象的风险指针,以通知其他线程删除该将产生实质风险。若程序不再需要那个对象,风险指针则被清零。 ^42568675-130-612-697
- ⏱ 2023-07-30 00:18:59
7.3.2 原则2:使用无锁的内存回收方案
- 📌 无锁数据是使用垃圾回收器的理想场景。若我们得以采用垃圾回收器,即事先知晓它具备适时删除无用节点的能力,则算法的实现代码写起来就会轻松一些。 ^42568675-136-664-733
- ⏱ 2023-07-30 00:09:47
7.3.3 原则3:防范ABA问题
- 📌 本章内容所涉及的算法均不存在ABA问题,但它很容易由无锁算法的代码引发。该问题最常见的解决方法之一是,在原子变量x中引入一个ABA计数器。将变量x和计数器组成单一结构,作为一个整体执行比较-交换操作。每当它的值被改换,计数器就自增。照此处理,如果别的线程改动了变量x,即便其值看起来与最初一样,比较-交换操作仍会失败。 ^42568675-137-1115-1274
- ⏱ 2023-07-30 00:14:04
8.2.2 数据竞争和缓存乒乓(cache ping-pong)[4]
-
📌 若另一线程正在另一处理器上运行相同的代码,两个处理器的缓存中将分别形成变量counter的副本,它们必须在两个处理器之间来回传递,两者所含的counter值才可以保持最新,从而正确执行自增操作。 ^42568675-147-1236-1333
- ⏱ 2023-07-30 23:20:59
-
📌 在上面代码的循环中,变量counter所含的数据在不同的缓存之间多次来回传递。这称为缓存乒乓,会严重影响应用程序的性能。 ^42568675-147-1553-1613
- ⏱ 2023-07-30 23:14:36
8.2.3 不经意共享
-
📌 通常,处理器的缓存单元并非独立的小片内存范围,而是连续的大块内存,称为缓存块。这些缓存块的大小往往是32字节或64字节,其准确值取决于我们使用的处理器的具体型号。缓存硬件只能按缓存块的大小来处理内存块,若多个小型数据项在内存中的位置彼此相邻,那它们将被纳入同一个缓存块。 ^42568675-148-370-505
- ⏱ 2023-07-30 23:26:17
-
📌 假定多个线程要访问一个整型数组(包括更新),其中各线程都具有专属的元素,且只反复读写自己的元素。由于整型变量的尺寸往往比缓存块小很多,因此同一缓存块能够容纳多个数组元素。结果,尽管各线程仅访问数组中属于自己的元素,但仍会让硬件产生缓存乒乓的现象。假定某缓存块内有两个元素,序号是0和1,每当有线程要更新0号元素,便需把整个缓存块传送到相关的处理器。若另一个处理器上的线程要更新1号元素,该缓存块则需再次传递。虽然两个线程没有共享任何数据,但缓存块却被它们共享,因此称之为不经意共享。这里的解决方法是编排数据布局,使得相同线程访问的数据在内存中彼此靠近,增加它们被纳入同一个缓存块的机会,而由不同线程访问的数据在内存中彼此远离,因此更有可能散布到多个独立的缓存块中。 ^42568675-148-637-969
- ⏱ 2023-07-30 23:39:16
10.1 并行化的标准库算法函数
- 📌 通过执行策略std::execution::par向标准库示意,准许该调用采用多线程,按并行算法的形式执行。 ^42568675-181-703-757
- ⏱ 2023-07-31 01:46:19
10.2.1 因指定执行策略而普遍产生的作用
-
📌 以下面的std::for_each()调用为例,它并未依从任何执行策略,异常std::bad_alloc会向外传播。 ^42568675-183-1381-1439
- ⏱ 2023-07-31 01:49:25
-
📌 然而,相应的重载版本依从某执行策略进行调用,则会令整个程序终止。 ^42568675-183-1565-1597
- ⏱ 2023-07-31 01:49:30
11.1.1 多余的阻塞
- 📌 活锁:与死锁类似,也是一个线程等待着另一个线程,而后者又反过来等待前者。两种情形的关键区别在于,这里发生的等待并非阻塞型等待,而是处于活动状态的检测循环,比如自旋锁。如果情况严重,活锁的表现与死锁相同(应用软件停滞不前)。但不同之处是,活锁令CPU占用率居高不下,因为牵涉的线程其实还都在运行,只不过互相阻碍着对方。而在不严重的情况下,活锁最终会因线程的随机调度而被解开。尽管这样,活锁仍会长久阻塞它所牵涉的任务,还会在此期间严重占用CPU。 ^42568675-193-793-1014
- ⏱ 2023-07-31 02:04:35
11.1.2 条件竞争
-
📌 数据竞争:这是一种特别的条件竞争。它的起因是对共享内存区域的并发访问未采取同步措施,结果导致未定义行为 ^42568675-194-588-639
- ⏱ 2023-07-31 02:05:59
-
📌 受到破坏的不变量:其表现形式为悬空指针(当前线程正在通过指针访问目标数据,而其他线程却同时删除指针)、随机的内存数据损坏(数据正更新到一半,而其他线程却同时读取,造成数据不一致)、重复释放内存(double-free,两个线程同时从队列弹出相同的值,它们都删除某份关联的数据)等。 ^42568675-194-740-880
- ⏱ 2023-07-31 02:07:22
11.2.1 审查代码并定位潜在错误
- 📌 若我们释放一个互斥,再重新获取,就必须假定已经有另一线程改动了共享数据。 ^42568675-196-2297-2333
- ⏱ 2023-07-31 02:13:38
读书笔记
5.3.3 原子操作的内存次序
划线评论
- 📌 若在read_x_then_y()函数中,变量y载入失败而返回false③,x的存储操作则必然发生在y的存储操作之前。在这种情形下,④处变量x的载入肯定返回true ^3533118-7K2Jket1n
- 💭 见 图5.3
- ⏱ 2023-07-28 15:42:16
