论坛首页 Java企业应用论坛

线程切换导致的效率问题

浏览 3333 次
精华帖 (0) :: 良好帖 (19) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2011-11-24  

最近在尝试把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失效。

   发表时间:2011-11-25  
LZ的替代方案,还可以进一步优化,比如使用一次CAS原子操作,减少操作系统锁调用。

yield策略和自旋锁,LZ可以做一下对比介绍?
0 请登录后投票
   发表时间:2011-11-28  
agapple 写道
LZ的替代方案,还可以进一步优化,比如使用一次CAS原子操作,减少操作系统锁调用。

yield策略和自旋锁,LZ可以做一下对比介绍?


现在write线程在竞争进入写操作时用trylock,这个是没有涉及到系统锁调用的,同样是cas操作sync state。

至于在竞争失败时yield N 次,而不使用自旋,主要是出于以下的考虑:
   1.write操作的时间是可预计的,但是如果数据块比较大,自旋时间会较久,浪费cpu。
   2.如果write线程超过cpu数,自旋会与真正进行写操作的线程竞争cpu。

所以保守使用yield。当然,在write线程少于cpu数(排除其他cpu密集线程),自旋比yield更轻,毕竟少了系统调用
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics