
C++并发编程
文章平均质量分 66
根据《C++并发实战》第二版为基础,记录讲解C++多线程并发编程。
不停感叹的老林_<C 语言编程核心突破>
大龄转行待业程序员, <C 语言编程核心突破> 助你迈入C的门槛
展开
-
2022-11-20 C++并发编程( 四十四 ) -- 通讯顺序进程 CSP 范型
并发编程除了常规的使用锁, 或无锁结构实现某些并发计算, 还有没有其它实现形式?有, 而且是非常成熟的并发编程范型, CSP 通讯顺序进程范型.掌握这种编程, 可以令业务逻辑梳理, 变得及其方便简单.如果没有学习过函数式编程, 可能初步了解起来稍有困难, 不过只要稍加思考, 一定会掌握.CSP, 也就是所谓通讯顺序进程, 就是用几个线程各自完成各自的逻辑, 每个线程都是状态机, 它们之间没有真正的共享数据, 只是通过相互通讯消息, 完成自己的业务.我们以银行 ATM 的运行逻辑, 来用图形梳理一下具体的运行原创 2022-11-22 17:42:27 · 1214 阅读 · 0 评论 -
2022-11-13 C++并发编程( 四十三 )
C++11 版本已经有很长一段时间了, 它把 C++ 带入了一个新的境地, 引入了一些有用的特性, 这些特性使得编写并发代码变得容易, 需要掌握, 并且以今天这个时点, 2022年, 这些特性已经极其成熟, 并非什么新鲜事物了.今天先介绍到这里.原创 2022-11-13 17:45:14 · 460 阅读 · 0 评论 -
2022-11-12 C++并发编程( 四十二 )
我们已经学习了 C++ 并发编程所需涉猎的绝大部分知识, 以下是给一张脑图, 把整体归纳一下, 可以作为辅助记忆工具.C++ 并发编程的所有内容, 已经讲述完毕, 难点颇多, 比如多线程乱序逻辑, 无锁并发容器, 多线程 debug, 还需多多实践.原创 2022-11-12 13:41:28 · 314 阅读 · 0 评论 -
2022-11-11 C++并发编程( 四十一 )
多线程测试较单线程测试难一些, 需要制造一个多线程并发同步环境, 多次测试, 因为并发的乱序, 很多结果是随机的, 需要足够多的次数.本文实现一个简单的并发测试结构, 用于测试并发代码.多线程测试由于存在不确定顺序, 通常结果是多样的, 需要多次测试.C++ 标准库可以很容易的通过 promise 和 future 达成同步, 同时利用 async 实现多线程, 析构对 future 进行线程汇入, 不必担心线程安全问题.原创 2022-11-11 15:09:45 · 604 阅读 · 0 评论 -
2022-11-08 C++并发编程( 四十 )
C++ 17 开始, 算法部分引入了并行算法的重载, 形式上的区别就是多了一个执行策略:并行算法可以提高效率, 但与普通算法的语义有所不同, 如果了解不深, 可能会得出你不想要的结果.比如浮点类型的运算, 哪怕是累加, 当顺序累加和并行累加时, 很可能结果是不同的.常用的并行算法, 可以直接使用标准库算法. 策略的使用, 要注意是否可使用同步. 并行算法依赖数据的分割, 对于不可交换, 及要求顺序性的操作, 需要小心.另外目前 Clang 还没有实现并行算法, 可使用 gcc 或 vs原创 2022-11-09 13:05:16 · 529 阅读 · 0 评论 -
2022-11-03 C++并发编程( 三十九 )
上篇文章我们介绍了线程池, 比较复杂, 本文会在线程池的基础上增加中断线程, 完善线程的较高级操作.线程的中断需要让线程主动而安全的停止, 通过信号的传入结束线程, 将其停止运行.线程中断部分已经讲解完成, 代码稍显复杂, 可中断线程类是可以不必配合线程池使用的, 但基本的运用方法和线程池差不多.原创 2022-11-04 16:13:56 · 618 阅读 · 0 评论 -
2022-10-27 C++并发编程( 三十八 )
我们已经介绍了 C++ 标准库有关并发编程的常用接口, 以及一些并发安全的数据结构, 算法, 接下来将要介绍的是有关线程的组织方法, 线程池.线程池的运用主要是减少由于不停的产生和消灭线程而带来的开销, 让固定数量的线程 ( 多核 CPU 支持的最大线程 ) 一直等待任务, 将任务封装后推入队列, 线程则通过工作队列的任务出队获取任务并执行.线程池的思想能够较为容易的避免不必要的线程开销, 对于并发编程, 是一个较为常见的技法, 需要掌握.原创 2022-10-28 15:20:43 · 490 阅读 · 0 评论 -
2022-10-24 C++并发编程( 三十七)
前面的文章介绍了一些算法的并行化, 本篇介绍另一种看似难以并行, 但经过推敲可以一部分并行的算法并行化.std::partial_sum() 函数是逐项累加, 举个例子:{ 0, 1, 2} 这个简单数组, 经逐项累加就是 { 0, 1, 3}, 即第二项是原第一项和第二项的和, 第三项是原一二三项的和.算法不难理解, 问题是貌似算法对计算顺序是有要求的, 前面我们说过, 对顺序有要求的算法难以并行化.原创 2022-10-25 10:03:35 · 625 阅读 · 0 评论 -
2022-10-22 C++并发编程( 三十六 )
前面文章介绍了并发累加算法, 本文根据前文的经验, 对 for_each, find 算法也并发化实现.算法还是分两种, 按 CPU 可用内核数量切分数据, 或通过递归切分数据. 代码符合基本的异常安全.两个比较简单的且容易并发化的函数中, 可以看到基本并发模式, 数据切割, 分配线程, 结果归拢, 这是基本的并发算法逻辑, 并不困难, 只是因为有异常安全等要求, 需要用一些手段.原创 2022-10-23 17:21:40 · 579 阅读 · 0 评论 -
2022-10-20 C++并发编程( 三十五 )
并发编程相对于单线程编程, 在异常安全上需要注意更多问题.我学C++时, 只是了解了异常, 异常捕获等等, 没有进行过深的研究, 也就是 C++ primer 所涉猎的部分, 仅此而已, 但书上没有阐明什么是真正的异常安全, 需要补充些前置知识.所谓异常安全, 意味着无资源泄漏风险, 无数据破坏风险.C++ 用 RAII 的方式巧妙的应对资源泄漏, 无论异常是否引发, 都会析构, 通过这种设计, 保证不会有资源泄漏.原创 2022-10-20 22:12:49 · 729 阅读 · 0 评论 -
2022-10-18 C++并发编程( 三十四 )
并发编程的技术细节已经基本介绍完, 现在可以研究一下并发编程的整体设计.通常的想法是将线程工作进行划分, 或按逻辑, 或直接按照处理数据的量等分, 本文就介绍一种利用栈结构分配线程的快速排序算法.通过递归的划分将数据压入栈中, 由栈弹出给多个线程, 然后每个线程再将数据压入栈, 如此循环, 直至达到结束递归条件.数据分配和线程分配的逻辑还是比较复杂的, 多线程并发的算法逻辑和单线程的算法逻辑也是有所区别, 而具体适用于哪种算法, 需要具体测试.原创 2022-10-18 13:32:11 · 277 阅读 · 0 评论 -
2022-10-11 C++并发编程(三十三)
前几期文章我们介绍了无锁并发栈的实现,接下来,再进一步,编写无锁队列。无锁数据结构说实在的,给你一个现成的实现,都不一定能看懂,更别说推断漏洞啥的,绝对是个费脑子的活。但有时候不得不去啃这块骨头,俗称造轮子,通过软磨硬泡,还是有办法搞懂的。本文中无锁队列的实现比无锁栈的实现要更难理解一些, 参考书中有些代码有 bug, 比如节点计数类, 如果是无符号类, 则内部计数会出现问题, 不能得负值, 可能会造成内存泄漏.};这个结构实现如果能啃下来, 至少在无锁编程方面, 应该是可以说说话了.原创 2022-10-13 14:30:17 · 1338 阅读 · 0 评论 -
2022-10-08 C++并发编程(三十二)
前文我们介绍了三种无锁并发栈的实现,管理内存回收的方法分别是:通过 pop 线程减少到 1 时的一次性释放,通过风险指针,通过智能指针的内部计数(并非真无锁)。本文介绍一种通过内部计数和外部计数实现的无锁并发栈,由 node 节点指针储存内部引用计数 internalCount,初始为 0,由 countedNodePtr 节点计数类封装 node 节点和 externalCount 外部引用计数,初始为 1.原创 2022-10-09 10:26:09 · 992 阅读 · 0 评论 -
2022-10-07 C++并发编程(三十一)
既然并发安全的无锁栈内存管理如此棘手,为什么不能用智能指针管理呢。好主意,因为确实很优雅,只不过有个小问题,正如我们在更早的文章讲过,std::shared_ptr 确实可以进行原子操作,但是这种操作并非是无锁的,对性能会有一定影响,只不过我们在代码中没有体现锁而已。辩证来看,无锁数据既然不是无阻塞的,那么引入个看不见的锁也不是啥问题,最重要的是优雅,至于性能,可以去做测试,可用就用,不可用就不用,仅此而已。在高性能和优雅之间,如果两者都能用,我宁愿选择优雅,如果对性能有要求,我们再想办法。原创 2022-10-07 20:50:01 · 267 阅读 · 0 评论 -
2022-10-06 C++并发编程(三十)
上文我们通过利用 pop 线程计数的方法,实现了无锁的并发栈。本文我们将用风险指针实现无锁并发栈。风险指针, 顾名思义,是鉴别要删除节点有无风险的信号指针。就像一家人看电视,所有人都看西游记,你要转台,得问问其他人同意不同意,如果不同意就先等等,等播完了再换。风险指针,理解起来不难,实现起来不容易,并且,这是一个有专利的算法,商业使用有点问题。原创 2022-10-06 21:51:35 · 861 阅读 · 0 评论 -
2022-09-30 C++并发编程(二十九)
无锁数据结构简单理解,就是不用锁完成并发安全的数据结构,其通常使用 CAS 即比较交换结构,原子操作 compare_exchange_weak() ,无锁不是无阻塞,完全无阻塞的乱序逻辑一般是无法满足并发逻辑的。本文我们介绍一个简单的无锁栈,说简单,但要比有锁栈难得多。无锁栈数据结构的一个难点是内存回收。原创 2022-10-04 12:09:19 · 768 阅读 · 2 评论 -
2022-09-29 C++并发编程(二十八)
链表也是以节点套节点的方式连接而成的经典数据结构,对于并发数据结构,链表有几个问题需要解决。首先,链表不能双向访问,以前的文章我们探讨过,对于一个可以从两个方向访问的链表,会最终在某一个时刻碰到同一个数据,并且一路自两个方向延续的加锁可能引发死锁。其次,无法使用迭代器,因为对于一个随时可能被其它线程改变的链表,不能保证迭代器的有效。在极度简化接口的情况下,基本处于可用,难点在于细颗粒度的传递节点及开关锁。原创 2022-09-29 20:45:14 · 259 阅读 · 0 评论 -
2022-09-28 C++并发编程(二十七)
在前文中,我们用多线程编写了简单的线程安全的栈和队列,因为这两种经典的数据结构接口足够简单,实现起来相对容易。接下来,我们试图设计一个基于有限桶的哈希表,并将其设计为适应并发环境的数据结构。其实C++ 是有自己的 hashmap 的,也就是 std::unordered_map 只不过并非适用于并发,当然如果改造只是粗暴的加一个锁,那并不是真正的并发数据结构,所以,需要重新设计。原创 2022-09-28 19:06:55 · 475 阅读 · 0 评论 -
2022-09-26 C++并发编程(二十六)
前文介绍了 C++ 标准库中的并发库,现在让我们了解一下基于锁的并发数据结构。并发安全的数据结构,其操作要么是不用改变任何元素,比如读操作,要么是改变元素时只让涉及改变的函数操作数据,其它的则通过锁进行阻塞。并发数据结构的设计有两个要点。其一,是要安全,为了安全,需要通过锁等设施干预程序运行逻辑,使得某些操作必须在一定条件下单线程运行。原创 2022-09-26 10:27:22 · 485 阅读 · 0 评论 -
2022-09-22 C++并发编程(二十五)
这是一个补充的文档,内容是线程闩 std::latch 和线程卡 std::barrier,在 C++ 20 引入。这两个类用于进行多线程的同步,原理是计数器,多线程的每个线程一旦触及到这两种同步对象的等待函数,就会阻塞,直到计数器变为 0 ,然后所有线程同时关闭阻塞,进入下一步。对于多个线程运行同一个函数,且需要等待某个条件完成的情况,可以用线程闩或线程卡,比较好用。只不过对于调试软件不够友好,使用此类型的程序,在lldb debug中会出现各种莫名的问题。原创 2022-09-22 21:41:22 · 1146 阅读 · 0 评论 -
2022-09-20 C++并发编程(二十四)
除了基本的原子操做内存次序,标准库还提供了内存栅栏 std::atomic_thread_fence(),用以灵活改变内存次序。内存栅栏可以插在原子操作之间,使得程序服从先行关系及同步关系,亦可插入普通操作与原子操作之间,同样可以令其服从内存次序。如果对前文的原子操作内存次序,及先行关系,同步关系等都掌握的话,本文则并不难理解。原子操作部分,介绍就到这里了。原创 2022-09-20 15:09:59 · 1080 阅读 · 0 评论 -
2022-07-25 C++并发编程(一)
C++11之后,并发库比较完善,可较为简单的进行并发编程,而不用直接调用系统api。使得并发编程较为方便,但并发编程的逻辑已经变了,已经由单线程的顺序逻辑转换为乱序逻辑,执行顺序需要自己把控。同时并发编程使得debug难度飙升,顺序逻辑为基础的gdb受到较大限制。并发编程的功用也并非只是追求高速度,对于程序的逻辑梳理,方便进行任务的并行处理也是不可或缺。并发编程的陷阱有时需要注意,需要并发的场合可能并非那么多,需要比较单线程和多线程的投入产出比。...原创 2022-07-26 10:15:21 · 241 阅读 · 0 评论 -
2022-07-27 C++并发编程(二)
线程归属权转移,在C++中,线程只可移动不可拷贝,有明确归属权。线程只支持移动,不支持拷贝,这是很容易理解的,也就是所谓的所有权转移,读者可简单练习体会。原创 2022-07-27 09:42:40 · 207 阅读 · 0 评论 -
2022-07-29 C++并发编程(三)
对于某些并发程序,不涉及用线程梳理流程问题,只涉及并发运算,那么其实际使用线程不应超过cpu内核数量,否则并不会提高效率。C++给每个线程实现了单一id,如果将线程设计为不同的使用目的,可通过线程id进行区别。设置合适的线程数量,以榨取最大cpu算力,辨别线程id,以梳理业务逻辑。使用C++线程库,很容易实现。...原创 2022-07-29 09:45:12 · 221 阅读 · 0 评论 -
2022-08-01 C++并发编程(四)
在C++多线程并发编程中,各个线程可以共享主线程全局变量,这既是优点,也是缺点。我们不用特殊机制即可在线程间共享数据,同样意味着,每个线程修改共享数据都会影响所有线程,引起数据竞争,导致计算结果不确定。因此我们需要了解互斥锁,条件变量等,用以梳理计算顺序,辅助程序逻辑,防止程序错误。多线程访问读写数据共享数据很容易,但做对,得到想要的结果很难。通过最简单的互斥锁,可以确保单一入口的函数对共享数据访问线程安全,然而一旦混合无锁入口,则线程就不安全了。...原创 2022-08-01 15:21:12 · 315 阅读 · 0 评论 -
2022-08-02 C++并发编程(五)
互斥锁对于多线程并发编程可以很好的保护数据,但有时效果会出乎意料,比如死锁。多线程死锁问题通常较难解决,很多时候是偶发出现,让人摸不着头脑,所以从设计开始,就应加以注意。原创 2022-08-02 21:24:11 · 415 阅读 · 0 评论 -
2022-08-03 C++并发编程(六)
锁的归属权转移也是一个值得研究的事情,比如我们处理一个数据,需要在不同函数中顺序处理,这是非常正常的事情,毕竟一个函数的功能有限。但在多线程中,有这么个问题,每个函数都是线程安全的,但是一旦组合,就不是线程安全的。为了整体的线程安全,需要不停的将锁的所有权转移给下一个承接的函数。同时控制锁的颗粒度,对于性能提升及其有意义,可防止因锁操作问题,导致多线程效率不如单线程效率。通过锁的所有权转移,延续线程的函数对数据的控制,起到保护作用,使得并发安全。...原创 2022-08-03 12:06:11 · 334 阅读 · 0 评论 -
2022-08-04 C++并发编程(七)
除了锁,C++还有其他方式保护共享数据,比如一些生成后就只读的数据,生成后极少更改的数据。此外,还有一种可递归的锁,虽然极少使用,甚至不推荐使用。对于只需初始化一次,后续不变的公有变量,我们 可以通过一次初始化而非一直加锁解决,对于读多写少的数据,可以使用读写锁解决。对于可能出现的递归加锁,我们有相应的工具,但通常是设计出了问题,需要理清逻辑更改设计,而非简单粗暴的递归加锁。...原创 2022-08-04 14:11:31 · 285 阅读 · 0 评论 -
2022-08-05 C++并发编程(八)
多线程逻辑是乱序的,然而很多时候,我们却需要一定的逻辑顺序,但这种顺序又不简单是单线程的一贯顺序,此时我们需要通过一些手段,来使得多线程变得有序。前几篇文章讲述了数据保护,用互斥锁确实可以达到某个线程等待另一个线程的目的,但这不是它的使用场景,我们将引入条件变量和 std::future.条件变量的引入,令多线程编程的逻辑顺序可以容易操控,同时要注意条件变量的伪唤醒。通过以前文章的锁,本文的条件变量,很容易实现符合多线程逻辑的队列容器,读者可根据自己需要进行改良。...原创 2022-08-05 12:10:07 · 186 阅读 · 0 评论 -
2022-08-06 C++并发编程(九)
我们设计程序时,有时会出现这种状况,一条主线,一条支线,两条线分别做不同的事情,最后合并为一支。我们可以用thread开启线程,但如果需要回传结果,稍显麻烦,此时可以用 std::async() 函数和 std::future 对象解决。当我们需要分离线程功能,可以通过标准库 std::async() 函数实现,也可通过多个线程进行任务包装 std::packaged_task, 用固定线程接收任务包,执行任务,实现稍复杂的线程功能分离逻辑。......原创 2022-08-09 10:16:15 · 427 阅读 · 0 评论 -
2022-08-11 C++并发编程(十)
std::promise 和 std::future 配合,可实现异步求值,提供数据的线程利用 std::promise 设置值,等待数据的线程会在 std::future 上阻塞线程,直到提供数据的线程设置关联值后,std::future 就绪。std::promise 和 std::future 的配对使用可以通过 promise 传值,future 获取,在线程间传递数据,同时future也可保存异常。......原创 2022-08-12 10:03:26 · 302 阅读 · 0 评论 -
2022-08-14 C++并发编程(十一)
在并发编程中,std::future 只可移动不可复制,有明确的所有权,但若多个线程同时需要访问一个 std::future 对象,会出现问题,std::shared_future 可很好的解决此问题。对于有所有权需求的程序,std::future 可以满足需求,对于需要多线程共享数据的,std::shared_future 可满足需求。...原创 2022-08-15 09:42:48 · 199 阅读 · 0 评论 -
2022-08-19 C++并发编程(十二)
多线程的程序执行时阻塞可以是某条件达成,也可以是限时等待。比如延时超时,等待某个时长后程序进行下一个语句,或者绝对超时,在某个时间点进行下一步执行语句。我们先从时钟类开始介绍。多线程除却条件阻塞,还可以运用延时阻塞实现程序运行逻辑,对于某些实现是必须掌握的。C++的时钟类已经较为全面,只是稍显复杂,但运用并不困难。结合future的等待函数即返回状态,可方便的进行延时逻辑的实现。原创 2022-08-19 21:31:51 · 394 阅读 · 0 评论 -
2022-08-20 C++并发编程(十三)
通过时钟类我们可以方便的查看程序运行时长,在程序优化时很有用处。结合条件变量,可以实现有条件逻辑的限时等待。同时支持限时等待的还有互斥,可以完成某些纯限时等待的简单逻辑。限时等待在多线程并发编程的作用是梳理程序的时间线逻辑,以完成某些有关时效性的程序逻辑实现,可粗略的分为两类,等待一段时间(for),或等到某个时间点(until)。单独使用并不复杂,和其他程序逻辑搭配,会复杂不少,还需仔细斟酌。原创 2022-08-21 12:10:45 · 651 阅读 · 0 评论 -
2022-08-22 C++并发编程(十四)
通过类似函数式编程的方式,将各个线程的数据传递由 future 完成,而非共享数据,可简化代码,捋顺程序逻辑。我们用蹩脚的函数式编程,完成了多线程的程序设计,虽然不一定如我们所愿能大幅提高效率,但作为示例,足够了。如何合理的设计程序,是否真的有必要启用多线程,可能更值得程序设计者优先考虑。原创 2022-08-22 20:16:22 · 358 阅读 · 0 评论 -
2022-08-25 C++ 并发编程(十五)
C++ 多线程并发编程的共享数据保护,底层细节来自于原子操作。原子操作顾名思义,是不可分割的操作,操作过程中不可被其他线程所见,状态只有没开始或已完成,不存在中间状态。标准原子类型的定义位于头文件内。配合6个内存次序语义,可以进行更多微操作提升运行效率。原子操作是多线程并发数据保护的底层操作,有利于更好的理解和设计并发程序。但操作相对繁琐,涉及内存相关知识,逻辑也较为复杂,需要多加训练方可理解。...原创 2022-08-26 09:26:48 · 678 阅读 · 0 评论 -
2022-08-29 C++并发编程(十六)
上文讲述了最简单的原子标识,本文讲讲 std::atomic 原子布尔类。相比较 std::atomic_flag ,原子布尔类有更多的操作。原子布尔类操作大都比较简单,只是比较交换操作较难理解,需要多学多练。原创 2022-08-29 21:04:10 · 3869 阅读 · 0 评论 -
2022-09-01 C++并发编程(十七)
std::atomic 是原子指针类,除了和原子布尔类相同的成员函数,还有更多的计算相关函数,如果对前两篇文章完全掌握,此篇文章的内容不难掌握。原子指针类相较原子布尔类,增加了一些运算操作,及重载运算符,不难理解,多用即会。...原创 2022-09-01 10:32:13 · 1057 阅读 · 0 评论 -
2022-09-04 C++并发编程(十八)
对于标准的整数(int, unsigned, long等),C++ 特化了标准整数原子类,相应会有一些算数函数,当然,有一些计算不包括在内,比如乘除。标准整数的原子操作大多可以如同普通整数操作一样方便,只是少了乘除等运算,其他更复杂的操作,直接上锁吧,原子操作一般做不来。如果是循序学习,标准整数的原子操作只是扩展了部分整数运算,及逻辑运算,新内容并不多,可根据示例仔细了解。原创 2022-09-04 13:04:12 · 452 阅读 · 0 评论 -
2022-09-05 C++并发编程(十九)
前述文章讲解了标准库有系统特化的原子类,如布尔类,指针类,整数类。但如我们所见,原子类是一个可更改对象类型的模板类,所以是可以进行泛化的,但是可转化为原子类的自定义类的条件较为苛刻。并且复杂的自定义类原子化后,无法保证无锁结构,并且如果有自定义的赋值和比较操作,都无法完成,通常还是要用互斥进行保护,而不是将其转化为原子类。用自定义类型创建原子类限制因素很多:不可含有虚函数,不可从虚基类派生,必须有编译器隐式生成拷贝赋值操作符,完全是 memcpy() ,原创 2022-09-06 10:28:20 · 452 阅读 · 0 评论