轉自:http://www.doserv.com/article/2012/0831/5299117.shtml
環境切換Context Switches
相對於資料拷貝影響的明顯,非常多的人會忽視了環境切換對效能的影響. 在我的經驗裡,比起資料拷貝,環境切換是讓高負載應用徹底完蛋的真正殺手. 系統更多的時間都花費線上程切換上,而不是花在真正做有用工作的線程上. 令人驚奇的是, (和資料拷貝相比)在同一個水平上,導致環境切換原因總是更常見. 引起環境切換的第一個原因往往是活躍線程數比CPU個數多. 隨著活躍線程數相對於CPU個數的增加,環境切換的次數也在增加,如果你夠幸運,這種增長是線性,但更常見是指數增長. 這個簡單的事實解釋了為什麼每個串連一個線程的多線程設計的延展性更差.
對於一個延展性的系統來說,限制活躍線程數少於或等於CPU個數是更有實際意義的方案. 曾經這種方案的一個變種是只使用一個活躍線程,雖然這種方案避免了環境爭用,同時也避免了鎖,但它不能有效利用多CPU在增加總輸送量上的價值,因此除非程式無CPU限制(non-CPU-bound), (通常是網路I/O限制 network-I/O-bound), 應該繼續使用更實際的方案.
一個有適量線程的程式首先要考慮的事情是規划出如何建立一個線程去管理多串連. 這通常意味著前置一個select/poll, 非同步I/O,訊號或者完成連接埠,而後台使用一個事件驅動的程式架構。關於哪種前置API是最好的有很多爭論. Dan Kegel的C10K在這個領域是一篇不錯的論文. 個人認為,select/poll和訊號通常是一種醜陋的方案,因此我更傾向於使用AIO或者完成連接埠,但是實際上它並不會好太多. 也許除了select(),它們都還不錯. 所以不要花太多精力去探索前置系統最外層內部到底發生了什麼.
對於最簡單的多線程事件驅動伺服器的概念性模型, 其內部有一個請求緩衝隊列,用戶端請求被一個或者多個監聽線程擷取後放到隊列裡,然後一個或者多個背景工作執行緒從隊列裡面取出請求並處理. 從概念上來說,這是一個很好的模型,有很多用這種方式來實現他們的代碼. 這會產生什麼問題嗎? 引起環境切換的第二個原因是把對請求的處理從一個線程轉移到另一個線程. 有些人甚至把對請求的回應又切換回最初的線程去做,這真是雪上加霜,因為每一個請求至少引起了2次環境切換. 把一個請求從監聽線程轉換到成背景工作執行緒,又轉換回監聽線程的過程中,使用一種"平滑"的方法來避免環境切換是非常重要的.
此時,是否把串連請求分配到多個線程,或者讓所有線程依次作為監聽線程來服務每個串連請求,反而不重要了.
即使在將來, 也不可能有辦法知道在伺服器中同一時刻會有多少啟用線程. 畢竟,每時每刻都可能有請求從任意串連發送過來,一些進行特殊任務的"後台"線程也會在任意時刻被喚醒. 那麼如果你不知道當前有多少線程是啟用的,又怎麼能夠限制啟用線程的數量呢?根據我的經驗,最簡單同時也是最有效方法之一是:用一個老式的帶計數的訊號量,每一個線程執行的時候就先持有訊號量. 如果訊號量已經到了最大值,那些處於監聽模式的線程被喚醒的時候可能會有一次額外的環境切換, (監聽線程被喚醒是因為有串連請求到來, 此時監聽線程持有訊號量時發現訊號量已滿,所以即刻休眠),
接著它就會被阻塞在這個訊號量上,一旦所有監聽模式的線程都這樣阻塞住了,那麼它們就不會再競爭資源了,直到其中一個線程釋放訊號量,這樣環境切換對系統的影響就可以忽略不計. 更主要的是,這種方法使大部分時間處於休眠狀態的線程避免在啟用線程數中佔用一個位置,這種方式比其它的替代方案更優雅.
一旦處理請求的過程被分成兩個階段(監聽和工作),那麼更進一步,這些處理過程在將來被分成更多的階段(更多的線程)就是很自然的事了. 最簡單的情況是一個完整的請求先完成第一步,然後是第二步(比如回應). 然而實際會更複雜: 一個階段可能產生出兩個不同執行路徑,也可能只是簡單的產生一個應答(例如返回一個緩衝的值). 由此每個階段都需要知道下一步該如何做,根據階段分發函數的傳回值有三種可能的做法:
請求需要被傳遞到另外一個階段(返回一個描述符或者指標)
請求已經完成(返回ok)
請求被阻塞(返回"請求阻塞")。這和前面的情況一樣,阻塞到直到別的線程釋放資源
應該注意到在這種模式下,對階段的排隊是在一個線程內完成的,而不是經由兩個線程中完成. 這樣避免不斷把請求放在下一階段的隊列裡,緊接著又從該隊列取出這個請求來執行。這種經由很多活動隊列和鎖的階段很沒必要.
這種把一個複雜的任務分解成多個較小的互相協作的部分的方式,看起來很熟悉,這是因為這種做法確實很老了. 我的方法,源於CAR在1978年發明的"通訊序列化進程" (Communicating Sequential Processes CSP),它的基礎可以上溯到1963時的Per Brinch Hansen and Matthew Conway--在我出生之前! 然而,當Hoare創造出CSP這個術語的時候,“進程”是從抽象的數學角度而言的,而且,這個CSP術語中的進程和作業系統中同名的那個進程並沒有關係.
依我看來,這種在作業系統提供的單個線程之內,實作類別似多線程一樣協同並發工作的CSP的方法,在可擴充性方面讓很多人頭疼.
一個實際的例子是,Matt Welsh的SEDA,這個例子表明分段執行的(stage-execution) 思想朝著一個比較合理的方向發展. SEDA是一個很好的 "server Aarchitecture done right" 的例子,值得把它的特性評論一下:
1. SEDA的批處理傾向於強調一個階段處理多個請求,而我的方式傾向於強調一個請求分成多個階段處理.
2. 在我看來SEDA的一個重大缺陷是給每個階段申請一個獨立的在載入響應階段中線程"後台"重分配的線程池. 結果,原因1和原因2引起的環境切換仍然很多.
3. 在純技術的研究項目中,在Java中使用SEDA是有用的,然而在實際應用場合,我覺得這種方法很少被選擇.