当前位置:首页 > 公众号精选 > CPP开发者
[导读]众所周知,STL容器不是线程安全的。对于vector,即使写方(生产者)是单线程写入,但是并发读的时候,由于潜在的内存重新申请和对象复制问题,会导致读方(消费者)的迭代器失效。实际表现也就是招致了coredump。另外一种情况,如果是多个写方,并发的push_back(),也会导...

众所周知,STL容器不是线程安全的。对于vector,即使写方(生产者)是单线程写入,但是并发读的时候,由于潜在的内存重新申请和对象复制问题,会导致读方(消费者)的迭代器失效。实际表现也就是招致了core dump。另外一种情况,如果是多个写方,并发的push_back(),也会导致core dump。

解法一

加锁是一种解决方案,比如互斥锁std::mutex。但是加std::mutex确实性能较差。对于多读少写的场景可以用读写锁(也叫共享独占锁)来缓解。比如C 17引入了std::shared_mutex 。更多锁的种类可以阅读之前写的这篇文章:

如何理解互斥锁、条件变量、读写锁以及自旋锁?

当然本文的目的自然不是自我重复再次介绍一次锁的使用,请继续阅读解法二!

解法二

更多的时候,其实可以通过固定vector的大小,避免动态扩容(无push_back)来做到lock-free!

即在开始并发读写之前(比如初始化)的时候,给vector设置好大小。

struct Data {
...
};
vector v;
v.resize(1000);
注意是resize(),不是reserve()

可能大家平时用reserve()比较多,顾名思义,reserve就是预留内存。为的是避免内存重新申请以及容器内对象的拷贝。说白了,reserve()是给push_back()准备的!

而resize除了预留内存以外,还会调用容器元素的构造函数,不仅分配了N个对象的内存,还会构造N个对象。从这个层面上来说,resize()在时间效率上是比reserve()低的。但是在多线程的场景下,用resize再合适不过。

你可以resize好N个对象,多线程不管是读还是写,都是通过容器的下标访问operator[]来访问元素,不要push_back()新元素。所谓的『写操作』在这里不是插入新元素,而是修改旧元素。

如果N的最大个数是可以预期的就直接设置就好,如果没办法预期就再把vector搞成ring buffer(环形队列)来缓解压力。

可以给元素类加上成员变量标记当前的读写状态、是否被消费等等。

当然,你会说,如果B,C,D,E,F这个5个线程是等价的,要不停消费vector中的元素,会造成重复消费不?

当然会。你可以把队列头的下标定义成原子变量(std::atomic),尽管原子变量也需要做线程同步,但是比一般的锁开销要小很多啦。

如果你想连原子变量也不用,有没有办法呢?有啊。那就给B,C,D,E,F分配不同的消费队列啊。比如当前有5个读线程,那么每个线程就消费下标对5取模之后的某个固定结果的下标。比如:

  • B消费:0、5、10、15、……
  • C消费:1、6、11、16、……
  • D消费:2、7、12、17、……
  • E消费:3、8、13、18、……
  • F消费:4、9、14、19、……
每个读线程各自维护自己当前消费的最新下标。

这样做有啥问题没?也有,就是可能会导致不同的线程繁忙和等待的情况差异巨大:忙的忙死,闲的闲死。具体场景具体分析,总之,无论如何要控制住。不要让一个任务hang住整个线程。

vector是顺序容器,STL中还有一类关联容器其线程安全问题也不容小觑。比如map、unordered_map。

我们可能会有这样一种场景:在并发环境下,收集一些Key-Value,存储在某一个公共的容器中。这里也谈一下不用锁的方案,当然做不到放之四海皆准。它有一些限制条件,只能看是否满足你的需要了。

当有多个写线程对情况下,并发地插入 map/unordered_map都会引发core dump。对此,在某些场景下也可以避免加锁:如果全量的key有办法在并发之前就能拿到的,那么就对这个map,提前做一下insert。

并发环境中如果只是修改value,而不是插入新key就不会core dump!不过如果你没办法保证多个写线程不会同时修改同一个key的value,那么可能存在value的覆盖。

无法保证这点时,还是需要加锁。不过可以对key采取某种hash策略转成整型,然后进行分段加锁,减少一点锁冲突的概率,或者用一下CAS的策略。

另外对于unordered_map,在单写多读的多线程场景下,会不会有问题呢?也可能有。gcc 4.7.2的unordered_map实现曾被爆出有这个问题。原因的新插入的元素,触发了rehash,让其他线程在unordered_map中查找的过程之中,出现了core dump。见:

https://stackoverflow.com/questions/16353334/segv-in-gccs-stdunordered-map

我不确定clang以及后续的gcc版本是否还有此问题。应该在不添加任何额外同步代码的情况下,无法解决。

容器并发前初始化与伪共享的争议

本文内容曾经在知乎上写过,有网友评论:解法二会有false sharing(伪共享)的问题。

这里简单回应一下,谈论伪共享,要考虑具体的场景。的确某些时候伪共享会带来性能损失,但是要和并行化带来的性能提升来比较,孰高孰低。如果并行提升的性能足够多,是足以弥补这点伪共享的损失的。

比如我要进行远程IO,我有N个key要查询redis,把他们的结果存储到一个vector中,这个vector的写入操作在IO的异步回调函数中。在不加任何额外处理的情况下,极大概率会导致vector的core dump。

而如果vector初始化一下,则无需在回调函数中加锁,就能保证安全。这时候并行IO本身带来的性能提升,远远大于可能的伪共享带来损失。

这里为什么说可能呢?因为伪共享的触发没你想象的这么简单。如何成功模拟出一次伪共享带来性能损失的例子?你可以写程序自测一下,并不容易……甚至你改一下优化级别,改成O2,测试表现都很不一样。

一般网络上谈论伪共享时所举的例子,并不是一个vector中多个元素之间并行读写触发了伪共享。而是vector的元素类型是一个对象,对象中有2个数据字段a和b,在多线程分别更新同一个元素的a和b字段的时候,导致了伪共享。

比如一个线程更新vector中每个元素的a字段,另外一个线程更新vector中每个元素的b字段。

Anyway,伪共享的议题比较复杂,欢迎留意评论!


- EOF -

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭