在我们的工作中,多线程编程是一件太稀松平常的事。在多线程环境下操作一个变量或者一块缓存,如果不对其操作加以限制,轻则变量值或者缓存内容不符合预期,重则会产生异常,导致进程崩溃。为了解决这个问题,操作系统提供了锁、信号量以及条件变量等几种线程同步机制供我们使用。如果每次操作都使用上述机制,在某些条件下(系统调用在很多情况下不会陷入内核),系统调用会陷入内核从而导致上下文切换,这样就会对我们的程序性能造成影响。
今天,借助此文,分享一下去年引擎优化的一个点,最终优化结果就是在多线程环境下访问某个变量,实现了无锁(lock-free)操作。
对于后端开发者来说,服务稳定性第一,性能第二,二者相辅相成,缺一不可。
作为IT开发人员,秉承着一句话:只要程序正常运行,就不要随便动。所以程序优化就一直被搁置,因为没有压力,所以就没有动力嘛。在去年的时候,随着广告订单数量越来越多,导致服务rt上涨,光报警邮件每天都能收到上百封,于是痛定思痛,决定优化一版。
秉承小步快跑的理念,决定从各个角度逐步优化,从简单到困难,逐个击破。所以在分析了代码之后,准备从锁这个角度入手,看看能否进行优化。
在进行具体的问题分析以及优化之前,先看下现有召回引擎的实现方案,后面的方案是针对现有方案的优化。
广告订单以HTTP方式推送给消息系统
消息系统收到广告订单消息后
将广告订单消息格式化后推送给消息队列kafka(第1步)
将广告订单消息持久化到DB(第2步)
召回引擎订阅kafka的topic
从kafka中实时获取广告订单消息,建立并实时更建立维度索引(第3步)
召回引擎接收pv流量,实时计算,并返回满足定向后的广告候选集(第4步)
从上面图中可以看出,召回引擎是一个多线程应用,一方面有个线程专门从kafka中获取最新的广告订单消息建立维度索引(此为写线程),另一方面,接收线上流量,根据流量属性,获取广告候选集(此为读线程)。因为召回引擎涉及到同时读和写同一块变量,因此读写不能同时操作。
在多线程环境下,对同一个变量访问,大致分为以下几种情况:
在上述几种情况中,多个线程同时读显然是线程安全的,而对于其他几种情况,则需要保证其_互斥排他_性,即读写不能同时进行,管他几个线程读几个线程写,代码走起。
thread1
{
std::lock_guard<std::mutex> lock(mtx);
// do sth(read or write)
}
thread2
{
std::lock_guard<std::mutex> lock(mtx);
// do sth(read or write)
}
threadN
{
std::lock_guard<std::mutex> lock(mtx);
// do sth(read or write)
}
在上述代码中,每一个线程对共享变量的访问,都会通过mutex来加锁操作,这样完全就避免了共享变量竞争的问题。
如果对于性能要求不是很高的业务,上述实现完全满足需求,但是对于性能要求很高的业务,上述实现就不是很好,所以可以考虑通过其他方式来实现。
我们设想一个场景,假如某个业务,写操作次数远远小于读操作次数,例如我们的召回引擎,那么我们完全可以使用读写锁来实现该功能,换句话说读写锁适合于读多写少的场景。
❝读写锁其实还是一种锁,是给一段临界区代码加锁,但是此加锁是在进行写操作的时候才会互斥,而在进行读的时候是可以共享的进行访问临界区的,其本质上是一种自旋锁。
❞
代码实现也比较简单,如下:
writer thread {
pthread_rwlock_wrlock(&rwlock);
// do write operation
pthread_rwlock_unlock(&rwlock);
}
reader thread2 {
pthread_rwlock_rdlock(&rwlock);
// do read operation
pthread_rwlock_unlock(&rwlock);
}
reader threadN {
pthread_rwlock_rdlock(&rwlock)
// do read operation
pthread_rwlock_unlock(&rwlock);
}
在此,说下读写锁的特性:
那么,对于一写多读的场景,还有没有可能进行再次优化呢?
答案是:有的。
下面,我们将针对一写多读,读多写少的场景,进行优化。
在上一节中,我们提到对于多线程访问,可以使用mutex对共享变量进行加锁访问。对于一写多读的场景,使用读写锁进行优化,使用读写锁,在读的时候,是不进行加锁操作的,但是当有写操作的时候,就需要加锁,这样难免也会产生性能上的影响,在本节,我们提供终极优化版本,目的是在写少读多的场景下实现lock-free。
如何在读写都存在的场景下实现lock-free呢?假设如果有两个共享变量,一个变量用来专供写线程来写,一个共享变量用来专供读线程来读,这样就不存在读写同步的问题了,如下所示:
在上节中,我们有提到,多个线程对一个变量同时进行读操作,是线程安全的。一个线程对一个变量进行写操作也是线程安全的(这不废话么,都没人跟它竞争),那么结合上述两点,上图就是线程安全的(多个线程读一个资源,一个线程写另外一个资源)。
好了,截止到现在,我们lock-free的雏形已经出来了,就是_使用双变量_来实现lock-free的目标。那么reader线程是如何第一时间能够访问writer更新后的数据呢?
❝假设有两个共享资源A和B,当前情况下,读线程正在读资源A。突然在某一个时刻,写线程需要更新资源,写线程发现资源A正在被访问,那么其更新资源B,更新完资源B后,进行切换,让读线程读资源B,然后写线程继续写资源A,这样就能完全实现了lock-free的目标,此种方案也可以称为双buffer方式。
❞
在上节中,我们提出了使用双buffer来实现lock-free的目标,那么如何实现读写buffer无损切换呢?
假设有两个资源,其指针分别为ptrA和ptrB,在某一时刻,ptrA所指向的资源正在被多个线程读,而ptrB所指向的资源则作为备份资源,此时,如果有写线程进行写操作,按照我们之前的思路,写完之后,马上启用ptrA作为读资源,然后写线程继续写ptrB所指向的资源,这样会有什么问题呢?
我们就以std::vector为例,如下图所示:
在上图左半部分,假设ptr指向读对象的指针,也就是说读操作只能访问ptr所指向的对象。
某一时刻,需要对对象进行写操作(删除对象Obj4),因为此时ptr = ptrA,因此写操作只能操作ptrB所指向的对象,在写操作执行完后,将ptr赋值为ptrB(保证后面所有的读操作都是在ptrB上),即保证当前ptr所指向的对象永远为最新操作,然后写操作去删除ptrA中的Obj4,但是此时,有个线程正在访问ptrA的Obj4,自然而然会轻则当前线程获取的数据为非法数据,重则程序崩溃。
❝此方案不可行,主要是因为在写操作的时候,没有判断当前是否还有读操作。
❞
在上述方案中,简单的变量交换,最终仍然可能存在读写同一个变量,进而导致崩溃。那么如果保证在写的时候,没有读是不是就能解决上述问题了呢?如果是的话,那么应该如何做呢?
显然,此问题就转换成如何判断一个对象上存在线程读操作。
用过std::shared_ptr的都知道,其内部有个成员函数use_count()来判断当前智能指针所指向变量的访问个数,代码如下:
long
_M_get_use_count() const noexcept
{
// No memory barrier is used here so there is no synchronization
// with other threads.
return __atomic_load_n(&_M_use_count, __ATOMIC_RELAXED);
}
那么,我们可以考虑采用智能指针的方案,代码也比较简单,如下:
std::atomic_size_t curr_idx = 0;
std::vector<std::shared_ptr<Obj>> obj_buffers;
obj_buffers.emplace_back(std::make_shared<Obj>(...));
obj_buffers.emplace_back(std::make_shared<Obj>(...));
// write thread
{
size_t prepare = 1 - curr_idx.load();
while (obj_buffers[prepare].use_count() > 1) {
continue;
}
obj_buffers[prepare]->load();
curr_idx = prepare;
}
// read thread
{
auto tmp = obj_buffers[curr_idx.load()];
// do sth
}
在上述代码中
首先创建一个vector,其内有两个Obj的智能指针,这俩智能指针所指向的Obj对象一个供读线程进行读操作,一个供写线程进行写操作
curr_idx代表当前可供读操作对象在obj_buffers的索引,即obj_buffers[curr_idx.load()]所指对象供读线程进行读操作
那么相应的,obj_buffers[1- curr_idx.load()]所指对象供写线程进行写操作
在读线程中
通过auto tmp = obj_buffers[curr_idx.load()];获取一个拷贝,由于obj_buffers中存储的是shared_ptr那么,该对象的引用计数+1
在tmp上进行读操作
在写线程中
prepare = 1 - curr_idx.load();在上面我有提到curr_idx指向可读对象在obj_buffers的索引,换句话说,1 - curr_idx.load()就是另外一个对象即可写对象在obj_buffers中的索引
通过while循环判断另外一个对象的引用计数是否大于1(如果大于1证明还有读线程正在进行读操作)
好了,截止到此,lock-free的实现目标基本已经完成。实现原理也也相对来说比较简单,重点是要保证写的时候没有读操作即可。
上图是召回引擎做了lock-free优化后的效果图,从图上来看,效果还是很明显的。
双buffer方案在“一写多读”的场景下能够实现lock-free的目标,那么对于“多写一读”或者“多写多读”场景,是否也能够满足呢?
答案是不太适合,主要是以下两个原因:
通过前面的章节,我们知道通过双buffer方式可以实现在一写多读场景下的lock-free,该方式要求两个对象或者buffer最终持有的数据是完全一致的,也就是说在单buffer情况下,只需要一个buffer持有数据就行,但是双buffer情况下,需要持有两份数据,所以存在内存浪费的情况。
其实说白了,双buffer实现lock-free,就是采用的空间换时间的方式。
双buffer方案在多线程环境下能较好的解决 “一写多读” 时的数据更新问题,特别是适用于数据需要定期更新,且一次更新数据量较大的情形。
性能优化是一个漫长的不断自我提升的过程,项目中的一点点优化往往就可以使得性能得到质的提升。
好了,今天的文章就到这,我们下期见。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/3MDkpEHZECopSChBvTvXiQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。