5. 多线程程序如何让 IO 和“计算”相互重叠,降低 latency?
基本思路是,把 IO 操作(通常是寫操作)通過 BlockingQueue 交給別的線程去做,自己不必等待。
例1: logging
在多線程服務(wù)器程序中,日志 (logging) 至關(guān)重要,本例僅考慮寫 log file 的情況,不考慮 log server。
在一次請求響應(yīng)中,可能要寫多條日志消息,而如果用同步的方式寫文件(fprintf 或 fwrite),多半會降低性能,因為:
- 文件操作一般比較慢,服務(wù)線程會等在 IO 上,讓 CPU 閑置,增加響應(yīng)時間。
- 就算有 buffer,還是不靈。多個線程一起寫,為了不至于把 buffer 寫錯亂,往往要加鎖。這會讓服務(wù)線程互相等待,降低并發(fā)度。(同時用多個 log 文件不是辦法,除非你有多個磁盤,且保證 log files 分散在不同的磁盤上,否則還是受到磁盤 IO 瓶頸制約。)
解決辦法是單獨用一個 logging 線程,負責寫磁盤文件,通過一個或多個 BlockingQueue 對外提供接口。別的線程要寫日志的時候,先把消息(字符串)準備好,然后往 queue 里一塞就行,基本不用等待。這樣服務(wù)線程的計算就和 logging 線程的磁盤 IO 相互重疊,降低了服務(wù)線程的響應(yīng)時間。
盡管 logging 很重要,但它不是程序的主要邏輯,因此對程序的結(jié)構(gòu)影響越小越好,最好能簡單到如同一條 printf 語句,且不用擔心其他性能開銷,而一個好的多線程異步 logging 庫能幫我們做到這一點。(Apache 的 log4cxx 和 log4j 都支持 AsyncAppender 這種異步 logging 方式。)
例2: memcached 客戶端
假設(shè)我們用 memcached 來保存用戶最后發(fā)帖的時間,那么每次響應(yīng)用戶發(fā)帖的請求時,程序里要去設(shè)置一下 memcached 里的值。這一步如果用同步 IO,會增加延遲。
對于“設(shè)置一個值”這樣的 write-only idempotent 操作,我們其實不用等 memcached 返回操作結(jié)果,這里也不用在乎 set 操作失敗,那么可以借助多線程來降低響應(yīng)延遲。比方說我們可以寫一個多線程版的 memcached 的客戶端,對于 set 操作,調(diào)用方只要把 key 和 value 準備好,調(diào)用一下 asyncSet() 函數(shù),把數(shù)據(jù)往 BlockingQueue 上一放就能立即返回,延遲很小。剩下的時就留給 memcached 客戶端的線程去操心,而服務(wù)線程不受阻礙。
其實所有的網(wǎng)絡(luò)寫操作都可以這么異步地做,不過這也有一個缺點,那就是每次 asyncWrite 都要在線程間傳遞數(shù)據(jù),其實如果 TCP 緩沖區(qū)是空的,我們可以在本線程寫完,不用勞煩專門的 IO 線程。Jboss 的 Netty 就使用了這個辦法來進一步降低延遲。
以上都僅討論了“打一槍就跑”的情況,如果是一問一答,比如從 memcached 取一個值,那么“重疊 IO”并不能降低響應(yīng)時間,因為你無論如何要等 memcached 的回復(fù)。這時我們可以用別的方式來提高并發(fā)度,見問題8。(雖然不能降低響應(yīng)時間,但也不要浪費線程在空等上,對吧)
另外以上的例子也說明,BlockingQueue 是構(gòu)建多線程程序的利器。
總結(jié)
以上是生活随笔為你收集整理的5. 多线程程序如何让 IO 和“计算”相互重叠,降低 latency?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 多线程多进程解析:Python、os、s
- 下一篇: 优先级反转解决方案