海量并发的无锁编程 (lock free programming)

简介:

最近在做在线架构的实现,在线架构和离线架构近线架构最大的区别是服务质量(SLA,Service Level Agreement,SLA 99.99代表10K的请求最多一次失败或者超时)和延时。而离线架构在意的是吞吐,SLA的不会那么严苛,比如99.9。离线架构一般要有流控,以控制用户发送请求的速度。以免多于服务端处理能力的请求造成大量的数据在buffer或者队列里堆积,造成大量的超时。在线架构不可能有流控了,你不能限制用户的请求。因此在线架构对于弹性扩容有很高的要求,在大量请求到来时自动扩展后台的服务能力。比如当前的请求已经占用了集群的70%的资源时,系统需要自动的扩容;相反,当前的请求仅仅占用了集群20%的资源时,有必要回收一部分资源了。要知道,公司机房的电费还是很贵的。

当然了在线和离线架构的相同和区别谈起来完全是一个大文章。本文主要关注在处理高并发请求的锁的使用上。几个原则吧:

  1. 不要使用全局锁。使用全局锁代表在需要请求锁时,其他为得到锁的线程都会等待,这将导致服务能力急剧下降。
  2. 一定要注意锁的作用范围,一定要保证锁作用于足够小的范围。一定不要在锁定区域有等待操作,比如IO调用。
  3. 尽量的考虑修改架构,避免加锁。

试想一个场景,为了服务质量,我们可能发送多个请求到后台,以达到:

  1. 高可用行,后台的某个节点挂了,有其他的backup request会被请求。如果节点的SLA是99%(很低了),那么发送2个请求到后台,SLA可以达到99.99%;如果单个节点的SLA是99.9%的话,SLA可以达到99.9999了,即百万次请求至多一次失败。
  2. 低延时,第一个回来的请求会响应,这样的话能够保证某些慢的节点不会影响系统整体的延时。

那么如何判断第一个请求是第一个达到的呢?

先想一个比较粗暴的办法:使用一个set记录未返回的request 的id,然后在接到响应时,查看这个set有没有这个id,如果有,删除它,并且响应client;第二个以后的响应达到时,由于在set已经没有这个id了,因此这些请求将被丢弃。

这个里边涉及到对set的读和写操作,这个需要加锁;如果这个set是进程内可见的,那么这个锁就是进程级别的(或者说该进程或者说是线程的子线程都是可见的),加锁时很多线程都会等待该锁。这样的话对性能会有很大损耗。

这个方法对于每秒几百次请求是没有问题的。但是如果达到千这个级别,那么锁的使用会达到数千次(比如1000个请求,发送3个请求到后台,那么每次写set加一次锁,3个请求回来都会加一次锁,因此相当于一个真实的请求会加锁4次,1000个请求就是4000次,想想都恐怖,1s要加锁4000次,锁的代价再小也很恐怖吧,别说set的插入和查询,删除也有不可忽略的性能损耗)。

那么可不可以加线程级别的锁?线程级别的锁会减少对其他线程的影响。但是,set如果也是线程级别的,那么得保证异步回调的借口也得是在同一个线程才可以。否则这个线程发出的请求,被其他的线程得到,那么上述的逻辑是不通的,因为set是线程级别的,对于其他线程来说是不可见的。这样的话如果架构能够保证一个异步请求的返回,也是在同一个线程处理就好了。那么,如果架构可以这么保证,那么你根本不需要锁,为什么呢?因为一个线程都是顺序执行的,不会有资源的竞争,因此读写set都是安全的,因此不需要加锁。

那么问题来了,架构如何支持这个异步回调也是走到相同的线程里?

一个实现就是实现一个线程池,对于特定的request id,基于一定的规则将他调度给一个工作线程;等到异步返回时,再通过这个request id调度给相同的线程处理。

那么如何实现一个线程池?boost 里有; 如果调度,boost 支持调度给哪个线程。问题解决。

睡觉。


当然了,你以为无锁编程会涉及CAS,那么可以移步 并发编程(三): 使用C++11实现无锁stack(lock-free stack)


目录
相关文章
|
缓存 Java 编译器
【并发编程的艺术】内存语义分析:volatile、锁与CAS
几个理解下面内容的关键点:cpu缓存结构、可见性、上一篇文章中的总线工作机制。通过系列的前面几篇文章,我们可以初步总结造成并发问题的原因,一是cpu本地内存(各级缓存)没有及时刷新到主存,二是指令重排序造成的执行乱序导致意料之外的结果,归根结底是对内存的使用不当导致的问题。
108 0
|
1月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
58 1
|
1月前
|
存储 缓存 编译器
Linux源码阅读笔记06-RCU机制和内存优化屏障
Linux源码阅读笔记06-RCU机制和内存优化屏障
|
1月前
|
缓存 算法 关系型数据库
BP-Wrapper:无锁竞争的替换算法系统框架
BP-Wrapper:无锁竞争的替换算法系统框架
22 2
|
3月前
|
算法 安全 Java
Java性能优化(五)-多线程调优-Lock同步锁的优化
基本特点Lock锁的基本操作通常基于乐观锁实现,尽管在某些情况下(如阻塞时)它也可能采用悲观锁的策略。通过对比图,我们可以清晰地看到两种同步锁的基本特点。Lock同步锁与Synchronized的比较在Java中,同步锁机制是确保多线程安全访问共享资源的重要手段。与JVM隐式管理锁的Synchronized相比,Lock同步锁(以下简称Lock锁)提供了更细粒度的控制,通过显式地获取和释放锁,为开发者提供了更大的灵活性。一、基本特点。
52 1
|
负载均衡 NoSQL 安全
Redis:6.0多线程无锁(lock-free)设计和多线程Reactor模式助力Redis QPS更上一层楼
Redis:6.0多线程无锁(lock-free)设计和多线程Reactor模式助力Redis QPS更上一层楼
|
安全 Java 调度
72. 对并发熟悉吗?说一下synchronized与Lock的区别与使用
72. 对并发熟悉吗?说一下synchronized与Lock的区别与使用
73 0
72. 对并发熟悉吗?说一下synchronized与Lock的区别与使用
|
XML 数据格式 程序员
非阻塞算法(Lock-Free)的实现
非阻塞算法(Lock-Free)的实现
|
消息中间件 Java C++
开源项目推荐:多进程和多线程的高性能消息队列(无锁队列),lock-free queue
开源项目推荐:多进程和多线程的高性能消息队列(无锁队列),lock-free queue
1064 0