Liuqichun's Blog

使用多线程的实践思考

本文源自一个线上问题引起的思考。诚然多线程是有益的,但使用不当反而会造成系统吞吐能力下降,甚至发生死锁。在使用多线程时我们可能面临下列情况:

  1. 当写的并发代码包含框架类的方法调用,总是可能存在线程安全问题,因为框架在不停升级,我们不能保证它一直是线程安全的;
  2. 线程配置不当会引起安全问题,例如:缓存队列溢出、瞬时任务增多导致线程池打满,我们的业务在不断变化,在一个新的上下文环境中,没有人能保证线程配置一直合理;
  3. 多线程让编程更复杂(需要处理更多情况),例如:控制执行顺序、并发访问变量;
  4. 在多线程中进行远程请求容易对下游服务(数据库服务或其他业务服务)造成压力;

通过以上,我们看到在项目中引入一种技术带来的额外风险,有时这种风险不是线性增长而是指数级别,因此从这些角度看应该谨慎使用多线程。好的实践是先寻找其他解决方案,最后再考虑使用多线程,把多线程当作性能扩展的最后一道防线。

如果不用多线程就不存在上述的问题。我们假设在使用多线程的背景下,来总结一些实践技巧。

扒一扒问题的根

多线程引起安全问题的根源是什么?

知道问题我们就离解决问题更近了一步,接下来分析一下这些问题。

对于第一类问题,站在计算机的角度是为了提升性能。计算机有多级存储体系,硬盘 -> 内存 -> CPU Cache,当 CPU Cache 不能及时失效就会导致运行的程序读取老旧数据,导致程序执行不符合预期,因此后来提出了内存一致模型并制定缓存一致性协议(e.g. MSI、MESI)来解决这类问题。

第二类问题出现在编译器编译和 CPU 执行指令期间。编译器为了提升性能,分析程序时会进行指令重排序,CPU 分析待执行的指令会调整顺序或并行执行(重排序缓冲区、超标量流水线)。一般编程语言都有自己的处理方式,例如 Java 构建了专属的内存模型(JSR 133、JSR 133 FAQ),在编译期间通过插入内存屏障阻止重排序,C++ 也有类似机制。CPU 通过提供特殊指令避免其重排序,编译器控制这些指令来实施。

第三类问题是线程调度的客观特性产生的,线程的调度通常是抢占式,资源分配不均容易引起死锁。目前解决死锁还没有好手段,大多是通过经验避免一些坑,或者通过死锁检测手段主动释放锁。

与锁的相爱相杀

在程序开发中,最忌讳使用不熟悉的技术,因为不可控会导致风险增加;在不熟悉的情况下使用相当于在程序中埋地雷,而程序员又菜又爱玩,等到系统出问题才悔悟。下面是用锁时的一些实践:

  1. 对于新手,请不要自己实现锁,尽量使用标准库或成熟框架中的锁
  2. 也不一定非用锁,使用并发安全的容器(Java)是不错的选择,也可以让类并发安全(例如无状态类)
  3. 考虑清楚场景再使用锁,例如:计算密集型、IO 密集型
  4. 考虑清楚异常情况下锁的释放,甚至发生死锁的情况,这些如何处理

使用多线程实践总结

总结一下平常使用多线程和锁的实践,避免出现低级错误,提升多线程的性能。

使用多线程:

使用锁:

(完)

<< Previous Post

|

Next Post >>

#多线程