线程模型优化

先说背景,抽象后线程模型为:

如上图线程模型,说明解释一下。多个线程接受请求,并发把请求提交到一个合并线程的队列中。提交请求伪代码表示为:

request.inTime = now
lock.lock
put queue
if(check)
 list = take top 20 require from queue
 doSomething(list)
 for require in list
  notify
 return  
else
 while(unNotify)
 {
  waitNotify
 }
 return
lock.unlock

简单解释说明一下伪代码,首先设置每个请求的inTime为当前时间。然后加锁,加锁成功后,将请求添加到队列队尾。判断check条件(后面再解释说明),如果满足check条件,则从队列中取出前20个(不够则只取出当前队列所有请求)。对这20个请求做批量操作。操作完后,对每个请求进行通知。如果check条件不满足,则循环等待通知,注意这里wait会释放锁。即等待由其他线程触发对该线程请求的处理,通知该线程。最后解锁。

同时,合并线程也有自旋操作,防止一直没有新请求,导致请求一直等待。操作基本步骤与上面一致,检测check条件,如果满足,则从队列中取出最多20条,批量处理,并依次通知。

在说明一下check操作内容,即当前队列长度>=20,或者当前队列的队首位置请求的inTime<当前时间戳-5ms。

如上所述,是抽象后的线程模型,简单说明即多个线程并发接受请求,将请求添加到一个合并线程的队列中,如果满足队列长度达到阈值,或者队列中最老的请求时间已经超过阈值,则在队列中取出最多n条批量操作,否则阻塞等待通知。合并线程自身也有自旋操作,防止一直没有新请求到来,导致队列请求无人触发操作,一直阻塞等待。并且接受请求线程数量一定大于队列长度阈值,因为每个线程请求操作为同步,要保证并发情况下可以满足队列长度阈值触发要求。

表面上看,这个多线程的操作模型,没有问题,可以达到并发和批量合并的效果。

但是实际测试中,却发现很多情况下,只有很少部分的请求可以合并,批量处理。大部分的请求都是单条操作,导致合并几乎没有效果,效率极低。

分析后,我们看这么一个场景:

假设队列长度阈值为m,等待时间阈值为s。

1.假设并发n(n>m)个请求几乎同时到来,前m-1个请求线程操作都不满足check条件,则都是加入队列后,wait等待通知,因为前面说过,wait等待是会释放锁的。则m-1个线程请求可以快速添加到队列中。

2.第m个请求,添加到队列后,满足check条件,则进行批量操作,此时批量操作耗时超过s。操作完后,通知其他线程。并释放锁。

3.注意,整个第m个请求的操作,是一直保持持有锁的状态,那么其他请求都会阻塞在加锁处,等待锁。而计算每个请求inTime又是在加锁之前,等待锁的时间一定是大于第m个请求操作的耗时,那么一定大于s。这就导致,第m个请求释放锁后,新拿到锁的请求,一定会满足check条件,即队列头的inTime已经超过阈值。而此时队列中就只有这一个请求,那么就只能对着一个请求进行处理。而对一个请求处理和批量处理的耗时几乎是一样的(这是必然的,否则合并请求批量处理就没有意义了)。

4.因为3中,单条处理时同样也是一直保持持有锁状态的,导致后面的请求也一直阻塞在加锁处。

如此这般,除了最开始可以合并请求批量处理,后面的请求,几乎都变为单条处理了。

那么,怎么优化这个线程模型呢?

首先,第一个想到的是计算inTime变为加锁成功后,但是这就会导致每个请求的耗时变得不准确,同时也违背了一些业务上的设计,所以不能使用。如果实际业务上对这个请求耗时不是要求很精确,可以采用这种方式。

因为这破坏了第三步的check条件,即第m个请求释放锁后,新拿到锁的请求,不再满足check条件,既可以快速添加队列后,释放锁。让其他线程可以拿到锁,添加到队列中。

因为我们不能这么做,只能从锁上进行处理,如果添加请求,不进行加锁,也可以解决这个问题。即使可能会导致,在check时,拿队列长度与阈值进行比较,队列长度不是最新的队列长度,这个也不会产生什么问题。而且添加队列,和从队列中取数据,本身队列也是有加锁操作的,所以也不会产生问题。

但是我们的批量操作是需要同步的,所以这种方法也不行。如果批量操作是可以并发的,也可以使用这种方式。

最后,我们只能从模型上进行处理,即添加队列时,不再进行check校验和触发批量操作,只是单纯的添加队列,就等待通知。这样就可以无锁化,去掉加锁操作了。

所有的check和触发操作,全部放到自旋处理中,这样就保证请求可以源源不断的添加到队列中,保证队列中请求的连续生产。在请求并发高时,自旋操作中的check可以快速满足队列长度阈值,进行批量合并操作。请求并发低的时候,自旋操作中的check也可以定时的满足时间阈值,也可以把队列中的请求全部取出进行批量合并操作。从另一个角度也解决了这个问题。

至此,线程模型优化描述完毕,当然,这个优化过程不是本人做的,是由本人组长优化并分享的。向组长致敬。

从这个优化过程中,我们也可以看出,每一个设计都不一定是完美的,对于每一个看似完美的设计都要保持怀疑的态度。而且在优化过程中,组长也做了大量的模拟和数据验证,所以对于每一次成功或者说是收获,都要付出劳动。

其实针对这部分优化,我也有思考过,但是都处于纸上谈兵的阶段,一直在进行代码和模型分析,也一直没有得出结论。再次检讨和反思,实践出真知,尤其是程序员这个行业,用数据说话,用实验证明。

发布了45 篇原创文章 · 获赞 21 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/ly262173911/article/details/103207862