线程阻塞唤醒导致的效率问题

最近在尝试把leveldb的移植到java上。对移植的leveldb测试写速度,测试场景分别用1,2,3,4同时并发压,每个线程写100w条数据。测试完成时间,结果是:1线程23s,2线程21s,3线程27s,4线程35s。1线程使用的时间出乎我的意料。一开始是怀疑在移植时,io的实现出问题,把写文件的操作屏蔽了,每个write操作到io那层都是马上返回。4个场景的完成时间都有所减少,但是1线程仍然和2线程耗时差不多。

先来看下leveldb的原生实现。leveldb是支持并发访问write的,在write的某些步骤(例如写log)用互斥锁来实现。每个write操作都会导致一次io调用。如果在数据体较少,但数据量非常大的情况下,频繁的io调用占的开销就很可观了。在移植时考虑到这个问题,实现了一个合并写的策略。描述如下:

在并发的write操作时,和leveldb的处理方法不同,每个write线程做完预处理后,提交一个write log请求到一个阻塞队里,然后阻塞等待,由一个专有线程不断从队里取出请求,在某些策略下(例如如果一个请求的数据非常小,并且队列不为空),合并请求的数据,一并写到log里。请求被处理后,唤醒write线程,write线程返回,至此一个write操作完成。

这种策略在高并发情况下,可以有效减少io次数。并且这个方案代码非常简单,几乎没有锁,因为在可能冲突的情况都是专有线程单线程处理。

问题应该是出在这个方案里,简单的用leveldb的原生思路替代这个方案后,1线程只需用8s。这个简单方案没考虑并发,多线程的场景就没有测。

继续分析合并写的方案。把write线程阻塞等待改为yield后,再对4个场景测试,耗时进一步减少,但1线程和2线程仍然没区别。更进一步修改,专有线程在阻塞队列为空时,不进入阻塞,采用yield。再测试,1线程耗时8秒。终于得到想要的结果。

在1个线程的场景下,专有线程在唤醒write线程后再取请求时都会进入阻塞状态。这是导致1个线程场景如此慢的原因。并发上去了,进入阻塞的次数就少了,所以出现1,2,3线程没有太大的差距。这里线程阻塞唤醒的开销之大出乎我的意料。

通过测试证明这种方案是不可取的,当write的操作不频繁时,会导致每个write时延非常高。

替代方案:

每个write线程做完预处理后,仍然是提交write请求到一个队列,区别是这里没有一个专有线程负责写log,write线程继续尝试竞争一个锁,成功的进入写log操作,从队里取出请求,适当合并数据,写log。竞争失败的线程检查自己的请求是否已被处理,是则返回,否则重复一定次数的yield。这种方案的成绩比较理想,4个场景分别为8s,15s,23s,28s。

 

附:后来看一篇 Martin Fowler介绍的一个lmax的玩意(http://martinfowler.com/articles/lmax.html)号称每秒能处理600w笔业务。好奇下,把里面的并发框架源码简单抠了下。简单来说,它的流程也是异步的,提交,等待。它里面的很多等待都有yield策略。如果针对cpu核数,适当的分配处理线程,使用yiled来避免阻塞,应该是一个应付高负载的较好的方案。当然,文章里还提到如此高的处理效率还得益于尽量避免cacheline失效。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值