在我们的工作中,多线程编程是一件太稀松平常的事。在多线程环境下操作一个变量或者一块缓存,如果不对其操作加以限制,轻则变量值或者缓存内容不符合预期,重则会产生异常,导致进程崩溃。为了解决这个问题,操作系统提供了锁、信号量以及条件变量等几种线程同步机制供我们使用。如果每次操作都使用上述机制,在某些条件下(系统调用在很多情况下不会陷入内核),系统调用会陷入内核从而导致上下文切换,这样就会对我们的程序性能造成影响。
今天,借助此文,分享一下去年引擎优化的一个点,最终优化结果就是在多线程环境下访问某个变量,实现了无锁(lock-free)操作。
背景
对于后端开发者来说,服务稳定性第一,性能第二,二者相辅相成,缺一不可。
作为IT开发人员,秉承着一句话:只要程序正常运行,就不要随便动。所以程序优化就一直被搁置,因为没有压力,所以就没有动力嘛😁。在去年的时候,随着广告订单数量越来越多,导致服务rt上涨,光报警邮件每天都能收到上百封,于是痛定思痛,决定优化一版。
秉承小步快跑的理念,决定从各个角度逐步优化,从简单到困难,逐个击破。所以在分析了代码之后,准备从锁这个角度入手,看看能否进行优化。
在进行具体的问题分析以及优化之前,先看下现有召回引擎的实现方案,后面的方案是针对现有方案的优化。
- 广告订单以HTTP方式推送给消息系统
- 消息系统收到广告订单消息后
- 将广告订单消息格式化后推送给消息队列kafka(第1步)
- 将广告订单消息持久化到DB(第2步)
- 召回引擎订阅kafka的topic
- 从kafka中实时获取广告订单消息,建立并实时更建立维度索引(第3步)
- 召回引擎接收pv流量,实时计算,并返回满足定向后的广告候选集(第4步)
从上面图中可以看出,召回引擎是一个多线程应用,一方面有个线程专门从kafka中获取最新的广告订单消息建立维度索引(此为写线程),另一方面,接收线上流量,根据流量属性,获取广告候选集(此为读线程)。因为召回引擎涉及到同时读和写同一块变量,因此读写不能同时操作。
概述
在多线程环境下,对同一个变量访问,大致分为以下几种情况:
- 多个线程同时读
- 多个线程同时写
- 一个线程写,一个线程读
- 一个线程写,多个线程读
- 多个线程写,一个线程读
- 多个线程写,多个线程读
在上述几种情况中,多个线程同时读显然是线程安全的,而对于其他几种情况,则需要保证其_互斥排他_性,即读写不能同时进行,管他几个线程读几个线程写,代码走起。