热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【性能优化】lockfree在召回引擎中的实现

在我们的工作中,多线程编程是一件太稀松平常的事。在多线程环境下操作一个变量或者一块缓存,如果不对其操作加以限制,轻则变量值或者缓存内容不符

在我们的工作中,多线程编程是一件太稀松平常的事。 在多线程环境下操作一个变量或者一块缓存,如果不对其操作加以限制,轻则变量值或者缓存内容不符合预期,重则会产生异常,导致进程崩溃。 为了解决这个问题,操作系统提供了锁、信号量以及条件变量等几种线程同步机制供我们使用。 如果每次操作都使用上述机制,在某些条件下(系统调用在很多情况下不会陷入内核),系统调用会陷入内核从而导致上下文切换,这样就会对我们的程序性能造成影响。

今天,借助此文,分享一下去年引擎优化的一个点,最终优化结果就是在多线程环境下访问某个变量,实现了无锁(lock-free)操作。

对于后端开发者来说,服务稳定性第一,性能第二,二者相辅相成,缺一不可。

作为IT开发人员,秉承着一句话:只要程序正常运行,就不要随便动。所以程序优化就一直被搁置,因为没有压力,所以就没有动力嘛:grin:。在去年的时候,随着广告订单数量越来越多,导致服务rt上涨,光报警邮件每天都能收到上百封,于是痛定思痛,决定优化一版。

秉承小步快跑的理念,决定从各个角度逐步优化,从简单到困难,逐个击破。所以在分析了代码之后,准备从锁这个角度入手,看看能否进行优化。

在进行具体的问题分析以及优化之前,先看下现有召回引擎的实现方案,后面的方案是针对现有方案的优化。

  • 广告订单以HTTP方式推送给消息系统

  • 消息系统收到广告订单消息后

    • 将广告订单消息格式化后推送给消息队列kafka(第1步)

    • 将广告订单消息持久化到DB(第2步)

  • 召回引擎订阅kafka的topic

    • 从kafka中实时获取广告订单消息,建立并实时更建立维度索引(第3步)

    • 召回引擎接收pv流量,实时计算,并返回满足定向后的广告候选集(第4步)

从上面图中可以看出,召回引擎是一个多线程应用,一方面有个线程专门从kafka中获取最新的广告订单消息建立维度索引(此为写线程),另一方面,接收线上流量,根据流量属性,获取广告候选集(此为读线程)。因为召回引擎涉及到同时读和写同一块变量,因此读写不能同时操作。

在多线程环境下,对同一个变量访问,大致分为以下几种情况:

  • 多个线程同时读

  • 多个线程同时写

  • 一个线程写,一个线程读

  • 一个线程写,多个线程读

  • 多个线程写,一个线程读

  • 多个线程写,多个线程读

在上述几种情况中,多个线程同时读显然是线程安全的,而对于其他几种情况,则需要保证其_互斥排他_性,即读写不能同时进行,管他几个线程读几个线程写,代码走起。

thread1
{std::lock_guard lock(mtx);// do sth(read or write)
}thread2
{std::lock_guard lock(mtx);// do sth(read or write)
}threadN
{std::lock_guard lock(mtx);// do sth(read or write)
}

在上述代码中,每一个线程对共享变量的访问,都会通过mutex来加锁操作,这样完全就避免了共享变量竞争的问题。

如果对于性能要求不是很高的业务,上述实现完全满足需求,但是对于性能要求很高的业务,上述实现就不是很好,所以可以考虑通过其他方式来实现。

我们设想一个场景,假如某个业务,写操作次数远远小于读操作次数,例如我们的召回引擎,那么我们完全可以使用读写锁来实现该功能,换句话说 读写锁适合于读多写少 的场景。

读写锁其实还是一种锁,是给一段临界区代码加锁,但是此加锁是在进行写操作的时候才会互斥,而在进行读的时候是可以共享的进行访问临界区的,其本质上是一种自旋锁。

代码实现也比较简单,如下:

writer thread {pthread_rwlock_wrlock(&rwlock);// do write operationpthread_rwlock_unlock(&rwlock);
}reader thread2 {pthread_rwlock_rdlock(&rwlock);// do read operationpthread_rwlock_unlock(&rwlock);
}reader threadN {pthread_rwlock_rdlock(&rwlock)// do read operationpthread_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> obj_buffers;
obj_buffers.emplace_back(std::make_shared(...));
obj_buffers.emplace_back(std::make_shared(...));// 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,所有新数据必须要等到索引切换时候才能使用,很可能达不到实时性要求

  • 多线程写时若用双buffer模式,则在索引切换时候也需要给对应的对象加锁,并且也要用类似于上面的while循环保证没有现成在执行写入操作时才能进行指针切换,而且此时也要等待读操作完成才能进行切换,这时候就对备用对象的锁定时间过长,在数据更新频繁的情况下是不合适的。

通过前面的章节,我们知道通过双buffer方式可以实现在一写多读场景下的lock-free,该方式要求两个对象或者buffer最终持有的数据是完全一致的,也就是说在单buffer情况下,只需要一个buffer持有数据就行,但是双buffer情况下,需要持有两份数据,所以存在内存浪费的情况。

其实说白了,双buffer实现lock-free,就是采用的空间换时间的方式。

双buffer方案在多线程环境下能较好的解决 “一写多读” 时的数据更新问题,特别是适用于数据需要定期更新,且一次更新数据量较大的情形。

性能优化是一个漫长的不断自我提升的过程,项目中的一点点优化往往就可以使得性能得到质的提升。

好了,今天的文章就到这,我们下期见。


推荐阅读
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板
    本文介绍了在Xamarin XAML语言中如何在页面级别构建ControlTemplate控件模板的方法和步骤,包括将ResourceDictionary添加到页面中以及在ResourceDictionary中实现模板的构建。通过本文的阅读,读者可以了解到在Xamarin XAML语言中构建控件模板的具体操作步骤和语法形式。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • Java和JavaScript是什么关系?java跟javaScript都是编程语言,只是java跟javaScript没有什么太大关系,一个是脚本语言(前端语言),一个是面向对象 ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • OpenMap教程4 – 图层概述
    本文介绍了OpenMap教程4中关于地图图层的内容,包括将ShapeLayer添加到MapBean中的方法,OpenMap支持的图层类型以及使用BufferedLayer创建图像的MapBean。此外,还介绍了Layer背景标志的作用和OMGraphicHandlerLayer的基础层类。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • Google在I/O开发者大会详细介绍Android N系统的更新和安全性提升
    Google在2016年的I/O开发者大会上详细介绍了Android N系统的更新和安全性提升。Android N系统在安全方面支持无缝升级更新和修补漏洞,引入了基于文件的数据加密系统和移动版本的Chrome浏览器可以识别恶意网站等新的安全机制。在性能方面,Android N内置了先进的图形处理系统Vulkan,加入了JIT编译器以提高安装效率和减少应用程序的占用空间。此外,Android N还具有自动关闭长时间未使用的后台应用程序来释放系统资源的机制。 ... [详细]
  • GPT-3发布,动动手指就能自动生成代码的神器来了!
    近日,OpenAI发布了最新的NLP模型GPT-3,该模型在GitHub趋势榜上名列前茅。GPT-3使用的数据集容量达到45TB,参数个数高达1750亿,训练好的模型需要700G的硬盘空间来存储。一位开发者根据GPT-3模型上线了一个名为debuid的网站,用户只需用英语描述需求,前端代码就能自动生成。这个神奇的功能让许多程序员感到惊讶。去年,OpenAI在与世界冠军OG战队的表演赛中展示了他们的强化学习模型,在限定条件下以2:0完胜人类冠军。 ... [详细]
author-avatar
不要芹菜味
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有