首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

11.6  减少上下文切换的开销

很多任务引入的操作都会发生阻塞;在运行和阻塞这两个状态之间转换需要使用上下文切换。服务器应用程序发生阻塞的一个缘由是在处理请求期间产生日志消息;为了了解如何通过减少上下文切换的开销来提高吞吐量,我们将对两种日志方案的调度行为进行分析。

大多数日志框架都是围绕println进行“瘦”包装的;当你有些事情需要记录日志的时候,仅仅需要把它打印出来就好了。另一个方案是第152页,LogWriter所显示的:日志记录的工作由一个专职的后台线程完成,而不是由请求线程完成。从开发人员的视角来看,两种方案基本上相同。但是性能上可能会有差别,取决于日志活动的工作量,即有多少线程正在记录日志,还取决于另一些因素,比如上下文切换的开销16。

日志操作的服务时间包括与输入/输出流相关的类花费的时间;如果I/O操作发生阻塞,它很可能包括线程被阻塞的时间。操作系统会把这个阻塞线程从调度队列中移出,直到I/O结束,并且很可能比这个花费的时间更长。当I/O结束的时候,其他线程可能被唤醒,并被允许完成它们的调度时间限额,并且在调度队列中有些线程可能已经排在我们前

面了——这会进一步增加服务时间。其他的情况可能是,如果多个线程同时记录日志,它们可能还会竞争输出流的锁,这样的情况下结果与阻塞的I/O是一样的——线程阻塞并等待锁,并被换出。这样的记录方式牵涉到了内联和加锁,会导致上下文切换的增多,以及服务时间的增加。

延长请求的服务时间是令人不快的,有以下几条原因。首先服务时间反映服务的质量:越长的服务时间意味着有人等待了更长的时间获得结果。但更重要的是,更长的服务时间在这里意味着更多锁的竞争。在11.4.1小节中,我们提到的“快进快出”原理告诉我们,应该尽可能短地占有锁,因为锁被占用的时间越长,它被争夺的可能性就越大。如果线程因为等待I/O发生阻塞,并且此时持有一个锁,另一个线程很可能想要得到这个锁,然而却已经被前一个线程得到了。并发系统在多数锁为非竞争锁的时候会有更好的性能,因为请求竞争性的锁意味着更多的上下文切换。代码如果造成更多的上下文切换意味着产生更少的吞吐量。

把I/O操作从请求处理线程中分离出来很可能缩短处理请求的平均服务时间。调用log的线程不再因等待输出流的锁,或者等待I/O完成而发生阻塞;它们只需使消息加入队列,然后返回到它们自己的任务。另一方面,我们已经向消息队列引入了竞争的可能性,但是put操作相对于日志的I/O(可能需要系统调用)是更加轻量的,并且在真实使用中更不易发生阻塞(只要队列没有填满)。因为请求线程现在不易发生阻塞了,因此在请求中间就会减少上下文被换出。我们完成的工作是把复杂、不确定的代码路径,包括I/O和锁竞争的可能性,转化为一致的代码路径。

进一步扩展,我们刚刚拆分了工作,把I/O操作移到了另一个线程,使用户看不到这个开销(这本身已经获得成功)。而且通过把所有的日志I/O移入一个线程,我们同样消除了输出流的竞争,因此又减少了竞争源。这改进了整体的吞吐量,因为需要调度的资源更少了,上下文切换更少了,锁的管理更简单了。

把I/O操作从请求处理线程中移到一个logger线程,就好像一个bucket brigade(排成长队以传水救火的队列)和一群跑来跑去想要救火的人之间的差别。在“上百个人拿着桶跑来跑去”的方案中,你对水源和火都存在着很大的竞争(结果是更少的水能够传递到灭火点),还会造成更低的效率,因为每一个工人都在不停的变换模式(注水、跑步、倒水、跑步,等等)。在bucket brigade的解决方案中,从水源到燃烧的建筑物间的水流是不断的,付出更少的体力却换得了更多的水传输过来。每个工人只负责自己的一项工作。如同中断对于人而言会造成麻烦,降低效率,阻塞和上下文切换会给线程造成麻烦。

总结

因为使用线程最主要的目的是利用多处理器资源,在并发程序性能的讨论中,我们通常更多地关注吞吐量和可伸缩性,而没有强调自然服务时间。Amdahl定律告诉我们,程序的可伸缩性是由必须连续执行的代码比例决定的。因为Java程序中串行化首要的来源是独占的资源锁,所以可伸缩性通常可以通过以下这些方式提升:减少用于获取锁的时间,减小锁的粒度,减少锁的占用时间,或者用非独占或非阻塞锁来取代独占锁。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论