5. 多线程程序如何让 IO 和“计算”相互重叠,降低 latency?
2017-03-01 14:01
1066 查看
基本思路是,把 IO 操作(通常是写操作)通过 BlockingQueue 交给别的线程去做,自己不必等待。
例1: logging
在多线程服务器程序中,日志 (logging) 至关重要,本例仅考虑写 log file 的情况,不考虑 log server。
在一次请求响应中,可能要写多条日志消息,而如果用同步的方式写文件(fprintf 或 fwrite),多半会降低性能,因为:
文件操作一般比较慢,服务线程会等在 IO 上,让 CPU 闲置,增加响应时间。
就算有 buffer,还是不灵。多个线程一起写,为了不至于把 buffer 写错乱,往往要加锁。这会让服务线程互相等待,降低并发度。(同时用多个 log 文件不是办法,除非你有多个磁盘,且保证 log files 分散在不同的磁盘上,否则还是受到磁盘 IO 瓶颈制约。)
解决办法是单独用一个 logging 线程,负责写磁盘文件,通过一个或多个 BlockingQueue 对外提供接口。别的线程要写日志的时候,先把消息(字符串)准备好,然后往 queue 里一塞就行,基本不用等待。这样服务线程的计算就和 logging 线程的磁盘 IO 相互重叠,降低了服务线程的响应时间。
尽管 logging 很重要,但它不是程序的主要逻辑,因此对程序的结构影响越小越好,最好能简单到如同一条 printf 语句,且不用担心其他性能开销,而一个好的多线程异步 logging 库能帮我们做到这一点。(Apache 的 log4cxx 和 log4j 都支持 AsyncAppender 这种异步 logging 方式。)
例2: memcached 客户端
假设我们用 memcached 来保存用户最后发帖的时间,那么每次响应用户发帖的请求时,程序里要去设置一下 memcached 里的值。这一步如果用同步 IO,会增加延迟。
对于“设置一个值”这样的 write-only idempotent 操作,我们其实不用等 memcached 返回操作结果,这里也不用在乎 set 操作失败,那么可以借助多线程来降低响应延迟。比方说我们可以写一个多线程版的 memcached 的客户端,对于 set 操作,调用方只要把 key 和 value 准备好,调用一下 asyncSet() 函数,把数据往 BlockingQueue 上一放就能立即返回,延迟很小。剩下的时就留给 memcached 客户端的线程去操心,而服务线程不受阻碍。
其实所有的网络写操作都可以这么异步地做,不过这也有一个缺点,那就是每次 asyncWrite 都要在线程间传递数据,其实如果 TCP 缓冲区是空的,我们可以在本线程写完,不用劳烦专门的 IO 线程。Jboss 的 Netty 就使用了这个办法来进一步降低延迟。
以上都仅讨论了“打一枪就跑”的情况,如果是一问一答,比如从 memcached 取一个值,那么“重叠 IO”并不能降低响应时间,因为你无论如何要等 memcached 的回复。这时我们可以用别的方式来提高并发度,见问题8。(虽然不能降低响应时间,但也不要浪费线程在空等上,对吧)
另外以上的例子也说明,BlockingQueue 是构建多线程程序的利器。
例1: logging
在多线程服务器程序中,日志 (logging) 至关重要,本例仅考虑写 log file 的情况,不考虑 log server。
在一次请求响应中,可能要写多条日志消息,而如果用同步的方式写文件(fprintf 或 fwrite),多半会降低性能,因为:
文件操作一般比较慢,服务线程会等在 IO 上,让 CPU 闲置,增加响应时间。
就算有 buffer,还是不灵。多个线程一起写,为了不至于把 buffer 写错乱,往往要加锁。这会让服务线程互相等待,降低并发度。(同时用多个 log 文件不是办法,除非你有多个磁盘,且保证 log files 分散在不同的磁盘上,否则还是受到磁盘 IO 瓶颈制约。)
解决办法是单独用一个 logging 线程,负责写磁盘文件,通过一个或多个 BlockingQueue 对外提供接口。别的线程要写日志的时候,先把消息(字符串)准备好,然后往 queue 里一塞就行,基本不用等待。这样服务线程的计算就和 logging 线程的磁盘 IO 相互重叠,降低了服务线程的响应时间。
尽管 logging 很重要,但它不是程序的主要逻辑,因此对程序的结构影响越小越好,最好能简单到如同一条 printf 语句,且不用担心其他性能开销,而一个好的多线程异步 logging 库能帮我们做到这一点。(Apache 的 log4cxx 和 log4j 都支持 AsyncAppender 这种异步 logging 方式。)
例2: memcached 客户端
假设我们用 memcached 来保存用户最后发帖的时间,那么每次响应用户发帖的请求时,程序里要去设置一下 memcached 里的值。这一步如果用同步 IO,会增加延迟。
对于“设置一个值”这样的 write-only idempotent 操作,我们其实不用等 memcached 返回操作结果,这里也不用在乎 set 操作失败,那么可以借助多线程来降低响应延迟。比方说我们可以写一个多线程版的 memcached 的客户端,对于 set 操作,调用方只要把 key 和 value 准备好,调用一下 asyncSet() 函数,把数据往 BlockingQueue 上一放就能立即返回,延迟很小。剩下的时就留给 memcached 客户端的线程去操心,而服务线程不受阻碍。
其实所有的网络写操作都可以这么异步地做,不过这也有一个缺点,那就是每次 asyncWrite 都要在线程间传递数据,其实如果 TCP 缓冲区是空的,我们可以在本线程写完,不用劳烦专门的 IO 线程。Jboss 的 Netty 就使用了这个办法来进一步降低延迟。
以上都仅讨论了“打一枪就跑”的情况,如果是一问一答,比如从 memcached 取一个值,那么“重叠 IO”并不能降低响应时间,因为你无论如何要等 memcached 的回复。这时我们可以用别的方式来提高并发度,见问题8。(虽然不能降低响应时间,但也不要浪费线程在空等上,对吧)
另外以上的例子也说明,BlockingQueue 是构建多线程程序的利器。
相关文章推荐
- 多线程WIN32程序如何检查死锁(三)-使用Petri网络来表示多线程应用程序
- 如何使用Java编写多线程程序
- 如何使用多线程进行计算
- 如何精确计算程序运行时间——精确获取时间(QueryPerformanceCounter)
- 如何设置HFSS13多线程计算!
- 多线程WIN32程序如何检查死锁(一)——死锁概述
- 如何计算Java程序运行时间
- C语言如何 计算程序运行时间?
- 如何利用windosAPI计算程序运行时间 不使用VC的库,也就是说不跨系统,跨编译器
- 区分异步和多线程应用场景(IO操作包括获取网络数据用异步,大量耗时的计算用线程)
- 如何在 C++ 程序中计算时间
- 请教高手:如何利用Delphi实现多线程圆周率的计算
- 如何创建带参数多线程程序
- 多线程WIN32程序如何检查死锁(二)-检查死锁的策略
- C# Mandelbrot和Julia分形图像生成程序更新到2010-9-14版 支持多线程计算 多核处理器
- 如何使用Java编写多线程程序
- 网络的大小是根据什么定,程序如何计算
- 多线程中如何保证某段程序 连续执行,不被其他线程干扰
- 图解:异步计算如何降低系统总体的响应时间
- 程序退出的各种方法,如何关闭多线程。