1. 程式人生 > >多執行緒伺服器的典型適用場合

多執行緒伺服器的典型適用場合

“伺服器開發”包羅永珍,本文所指的“伺服器開發”一句話形容是:跑在多核機器上的 Linux 使用者態的沒有使用者介面的長期執行的網路應用程式。“長期執行”的意思不是指程式 7x24 不重啟,而是程式不會因為無事可做而退出,它會等著下一個請求的到來。例如 wget 不是長期執行的,httpd 是長期執行的。

正名

與前文相同,本文的“程序”指的是 fork() 系統呼叫的產物。“執行緒”指的是 pthread_create() 的產物,而且我指的 pthreads 是 NPTL 的,每個執行緒由 clone() 產生,對應一個核心的 task_struct。本文所用的開發語言是 C++,執行環境為 Linux。

首先,一個由多臺機器組成的分散式系統必然是多程序的(字面意義上),因為程序不能跨 OS 邊界。在這個前提下,我們把目光集中到一臺機器,一臺擁有至少 4 個核的普通伺服器。如果要在一臺多核機器上提供一種服務或執行一個任務,可用的模式有:

  1. 執行一個單執行緒的程序
  2. 執行一個多執行緒的程序
  3. 執行多個單執行緒的程序
  4. 執行多個多執行緒的程序

這些模式之間的比較已經是老生常談,簡單地總結:

  • 模式 1 是不可伸縮的 (scalable),不能發揮多核機器的計算能力;
  • 模式 3 是目前公認的主流模式。它有兩種子模式:
    • 3a 簡單地把模式 1 中的程序執行多份,如果能用多個 tcp port 對外提供服務的話;
    • 3b 主程序+woker程序,如果必須繫結到一個 tcp port,比如 httpd+fastcgi。
  • 模式 2 是很多人鄙視的,認為多執行緒程式難寫,而且不比模式 3 有什麼優勢;
  • 模式 4 更是千夫所指,它不但沒有結合 2 和 3 的優點,反而匯聚了二者的缺點。

本文主要想討論的是模式 2 和模式 3b 的優劣,即:什麼時候一個伺服器程式應該是多執行緒的。

從功能上講,沒有什麼是多執行緒能做到而單執行緒做不到的,反之亦然,都是狀態機嘛(我很高興看到反例)。從效能上講,無論是 IO bound 還是 CPU bound 的服務,多執行緒都沒有什麼優勢。那麼究竟為什麼要用多執行緒?

在回答這個問題之前,我先談談必須用必須用單執行緒的場合。

必須用單執行緒的場合

據我所知,有兩種場合必須使用單執行緒:

  1. 程式可能會 fork()
  2. 限制程式的 CPU 佔用率

fork() 一般不能在多執行緒程式中呼叫,因為 Linux 的 fork() 只克隆當前執行緒的 thread of control,不克隆其他執行緒。也就是說不能一下子 fork() 出一個和父程序一樣的多執行緒子程序,Linux 也沒有 forkall() 這樣的系統呼叫。forkall() 其實也是很難辦的(從語意上),因為其他執行緒可能等在 condition variable 上,可能阻塞在系統呼叫上,可能等著 mutex 以跨入臨界區,還可能在密集的計算中,這些都不好全盤搬到子程序裡。

更為糟糕的是,如果在 fork() 的一瞬間某個別的執行緒 a 已經獲取了 mutex,由於 fork() 出的新程序裡沒有這個“執行緒a”,那麼這個 mutex 永遠也不會釋放,新的程序就不能再獲取那個 mutex,否則會死鎖。(這一點僅為推測,還沒有做實驗,不排除 fork() 會釋放所有 mutex 的可能。)

綜上,一個設計為可能呼叫 fork() 的程式必須是單執行緒的,比如我在《啟示》一文中提到的“看門狗程序”。多執行緒程式不是不能呼叫 fork(),而是這麼做會遇到很多麻煩,我想不出做的理由。

一個程式 fork() 之後一般有兩種行為:

  1. 立刻執行 exec(),變身為另一個程式。例如 shell 和 inetd;又比如 lighttpd fork() 出子程序,然後執行 fastcgi 程式。或者叢集中執行在計算節點上的負責啟動 job 的守護程序(即我所謂的“看門狗程序”)。
  2. 不呼叫 exec(),繼續運行當前程式。要麼通過共享的檔案描述符與父程序通訊,協同完成任務;要麼接過父程序傳來的檔案描述符,獨立完成工作,例如 80 年代的 web 伺服器 NCSA httpd。

這些行為中,我認為只有“看門狗程序”必須堅持單執行緒,其他的均可替換為多執行緒程式(從功能上講)。

單執行緒程式能限制程式的 CPU 佔用率。

這個很容易理解,比如在一個 8-core 的主機上,一個單執行緒程式即便發生 busy-wait(無論是因為 bug 還是因為 overload),其 CPU 使用率也只有 12.5%,即佔滿 1 個 core。在這種最壞的情況下,系統還是有 87.5% 的計算資源可供其他服務程序使用。

因此對於一些輔助性的程式,如果它必須和主要功能程序執行在同一臺機器的話(比如它要監控其他服務程序的狀態),那麼做成單執行緒的能避免過分搶奪系統的計算資源。

基於程序的分散式系統設計

《常用模型》一文提到,分散式系統的軟體設計和功能劃分一般應該以“程序”為單位。我提倡用多執行緒,並不是說把整個系統放到一個程序裡實現,而是指功能劃分之後,在實現每一類服務程序時,在必要時可以藉助多執行緒來提高效能。對於整個分散式系統,要做到能 scale out,即享受增加機器帶來的好處。

對於上層的應用而言,每個程序的程式碼量控制在 10 萬行 C++ 以下,這不包括現成的 library 的程式碼量。這樣每個程序都能被一個腦子完全理解,不會出現混亂。(其實我更想說 5 萬行。)

本文繼續討論一個服務程序什麼時候應該用多執行緒,先說說單執行緒的優勢。

單執行緒程式的優勢

從程式設計的角度,單執行緒程式的優勢無需贅言:簡單。程式的結構一般如《常用模型》所言,是一個基於 IO multiplexing 的 event loop。或者如雲風所言,直接用阻塞 IO。

event loop 的典型程式碼框架是:

while (!done) { 
  int retval = ::poll(fds, nfds, timeout_ms); 
  if (retval < 0) { 
    處理錯誤 
  } else { 
    處理到期的 timers 
    if (retval > 0) { 
      處理 IO 事件 
    } 
  } 
}

event loop 有一個明顯的缺點,它是非搶佔的(non-preemptive)。假設事件 a 的優先順序高於事件 b,處理事件 a 需要 1ms,處理事件 b 需要 10ms。如果事件 b 稍早於 a 發生,那麼當事件 a 到來時,程式已經離開了 poll() 呼叫開始處理事件 b。事件 a 要等上 10ms 才有機會被處理,總的響應時間為 11ms。這等於發生了優先順序反轉。

這可缺點可以用多執行緒來克服,這也是多執行緒的主要優勢。

多執行緒程式有效能優勢嗎?

前面我說,無論是 IO bound 還是 CPU bound 的服務,多執行緒都沒有什麼絕對意義上的效能優勢。這裡詳細闡述一下這句話的意思。

這句話是說,如果用很少的 CPU 負載就能讓的 IO 跑滿,或者用很少的 IO 流量就能讓 CPU 跑滿,那麼多執行緒沒啥用處。舉例來說:

  1. 對於靜態 web 伺服器,或者 ftp 伺服器,CPU 的負載較輕,主要瓶頸在磁碟 IO 和網路 IO。這時候往往一個單執行緒的程式(模式 1)就能撐滿 IO。用多執行緒並不能提高吞吐量,因為 IO 硬體容量已經飽和了。同理,這時增加 CPU 數目也不能提高吞吐量。
  2. CPU 跑滿的情況比較少見,這裡我只好虛構一個例子。假設有一個服務,它的輸入是 n 個整數,問能否從中選出 m 個整數,使其和為 0 (這裡 n < 100, m > 0)。這是著名的 subset sum 問題,是 NP-Complete 的。對於這樣一個“服務”,哪怕很小的 n 值也會讓 CPU 算死,比如 n = 30,一次的輸入不過 120 位元組(32-bit 整數),CPU 的運算時間可能長達幾分鐘。對於這種應用,模式 3a 是最適合的,能發揮多核的優勢,程式也簡單。

也就是說,無論任何一方早早地先到達瓶頸,多執行緒程式都沒啥優勢。

說到這裡,可能已經有讀者不耐煩了:你講了這麼多,都在說單執行緒的好處,那麼多執行緒究竟有什麼用?

適用多執行緒程式的場景

我認為多執行緒的適用場景是:提高響應速度,讓 IO 和“計算”相互重疊,降低 latency

雖然多執行緒不能提高絕對效能,但能提高平均響應效能。

一個程式要做成多執行緒的,大致要滿足:

  • 有多個 CPU 可用。單核機器上多執行緒的優勢不明顯。
  • 執行緒間有共享資料。如果沒有共享資料,用模型 3b 就行。雖然我們應該把執行緒間的共享資料降到最低,但不代表沒有;
  • 共享的資料是可以修改的,而不是靜態的常量表。如果資料不能修改,那麼可以在程序間用 shared memory,模式 3 就能勝任;
  • 提供非均質的服務。即,事件的響應有優先順序差異,我們可以用專門的執行緒來處理優先順序高的事件。防止優先順序反轉;
  • latency 和 throughput 同樣重要,不是邏輯簡單的 IO bound 或 CPU bound 程式;
  • 利用非同步操作。比如 logging。無論往磁碟寫 log file,還是往 log server 傳送訊息都不應該阻塞 critical path;
  • 能 scale up。一個好的多執行緒程式應該能享受增加 CPU 數目帶來的好處,目前主流是 8 核,很快就會用到 16 核的機器了。
  • 具有可預測的效能。隨著負載增加,效能緩慢下降,超過某個臨界點之後急速下降。執行緒數目一般不隨負載變化。
  • 多執行緒能有效地劃分責任與功能,讓每個執行緒的邏輯比較簡單,任務單一,便於編碼。而不是把所有邏輯都塞到一個 event loop 裡,就像 Win32 SDK 程式那樣。

這些條件比較抽象,這裡舉一個具體的(雖然是虛構的)例子。

假設要管理一個 Linux 伺服器機群,這個機群裡有 8 個計算節點,1 個控制節點。機器的配置都是一樣的,雙路四核 CPU,千兆網互聯。現在需要編寫一個簡單的機群管理軟體(參考 LLNL 的 SLURM),這個軟體由三個程式組成:

  • 執行在控制節點上的 master,這個程式監視並控制整個機群的狀態。
  • 運在每個計算節點上的 slave,負責啟動和終止 job,並監控本機的資源。
  • 給終端使用者的 client 命令列工具,用於提交 job。

根據前面的分析,slave 是個“看門狗程序”,它會啟動別的 job 程序,因此必須是個單執行緒程式。另外它不應該佔用太多的 CPU 資源,這也適合單執行緒模型。

master 應該是個模式 2 的多執行緒程式:

  • 它獨佔一臺 8 核的機器,如果用模型 1,等於浪費了 87.5% 的 CPU 資源。
  • 整個機群的狀態應該能完全放在記憶體中,這些狀態是共享且可變的。如果用模式 3,那麼程序之間的狀態同步會成大問題。而如果大量使用共享記憶體,等於是掩耳盜鈴,披著多程序外衣的多執行緒程式。
  • master 的主要效能指標不是 throughput,而是 latency,即儘快地響應各種事件。它幾乎不會出現把 IO 或 CPU 跑滿的情況。
  • master 監控的事件有優先順序區別,一個程式正常執行結束和異常崩潰的處理優先順序不同,計算節點的磁碟滿了和機箱溫度過高這兩種報警條件的優先順序也不同。如果用單執行緒,可能會出現優先順序反轉。
  • 假設 master 和每個 slave 之間用一個 TCP 連線,那麼 master 採用 2 個或 4 個 IO 執行緒來處理 8 個 TCP connections 能有效地降低延遲。
  • master 要非同步的往本地硬碟寫 log,這要求 logging library 有自己的 IO 執行緒。
  • master 有可能要讀寫資料庫,那麼資料庫連線這個第三方 library 可能有自己的執行緒,並回調 master 的程式碼。
  • master 要服務於多個 clients,用多執行緒也能降低客戶響應時間。也就是說它可以再用 2 個 IO 執行緒專門處理和 clients 的通訊。
  • master 還可以提供一個 monitor 介面,用來廣播 (pushing) 機群的狀態,這樣使用者不用主動輪詢 (polling)。這個功能如果用單獨的執行緒來做,會比較容易實現,不會搞亂其他主要功能。
  • master 一共開了 10 個執行緒:
    • 4 個用於和 slaves 通訊的 IO 執行緒
    • 1 個 logging 執行緒
    • 1 個數據庫 IO 執行緒
    • 2 個和 clients 通訊的 IO 執行緒
    • 1 個主執行緒,用於做些背景工作,比如 job 排程
    • 1 個 pushing 執行緒,用於主動廣播機群的狀態
  • 雖然執行緒數目略多於 core 數目,但是這些執行緒很多時候都是空閒的,可以依賴 OS 的程序排程來保證可控的延遲。

綜上所述,master 用多執行緒方式編寫是自然且高效的。

執行緒的分類

據我的經驗,一個多執行緒服務程式中的執行緒大致可分為 3 類:

  1. IO 執行緒,這類執行緒的的主迴圈是 io multiplexing,等在 select/poll/epoll 系統呼叫上。這類執行緒也處理定時事件。當然它的功能不止 IO,有些計算也可以放入其中。
  2. 計算執行緒,這類執行緒的主迴圈是 blocking queue,等在 condition variable 上。這類執行緒一般位於 thread pool 中。
  3. 第三方庫所用的執行緒,比如 logging,又比如 database connection。

伺服器程式一般不會頻繁地啟動和終止執行緒。甚至,在我寫過的程式裡,create thread 只在程式啟動的時候呼叫,在服務執行期間是不呼叫的。

在多核時代,多執行緒程式設計是不可避免的,“鴕鳥演算法”不是辦法。

相關推薦

【muduo】執行伺服器適用場合與程式設計模型

文章目錄 一、程序與執行緒 1、程序的概念 2、關於程序的一個形象比喻(人) 3、執行緒的概念 二、多程序和多執行緒的適用場景 1、需要頻繁建立銷燬的優先用執行緒 2、

執行伺服器適用場合》例釋與答疑

2010 March 3 - rev 01 《多執行緒伺服器的適用場合》(以下簡稱《適用場合》)一文在部落格登出之後,有熱心讀者提出質疑,我自己也覺得原文沒有把道理說通說透,這篇文章試圖用一些例項來解答讀者的疑問。我本來打算修改原文,但是考慮到已經讀過的讀者不一定

執行伺服器典型適用場合

“伺服器開發”包羅永珍,本文所指的“伺服器開發”一句話形容是:跑在多核機器上的 Linux 使用者態的沒有使用者介面的長期執行的網路應用程式。“長期執行”的意思不是指程式 7x24 不重啟,而是程式不會因為無事可做而退出,它會等著下一個請求的到來。例如 wget 不是長期

Qt TCP通訊,執行伺服器

相信許多初學Qt的同學都會和我一樣遇到這樣的問題: 一、Qt TCP通訊在使用nextPendingConnect後,伺服器端就只會與最後接入的客戶端通訊,這個時候就會考慮繼承QThread實現多執行緒,從而實現多個客戶端與伺服器端通訊,每當一個新的客戶端連線時,通過標識碼socke

用 threading 寫執行伺服器

import socket import threading   server = socket.socket() server.bind(("127.0.0.1",8899)) server.listen(1000)   def func(conn):   while T

執行伺服器

#coding=utf-8 from socket import * from threading import Thread from time import sleep # 處理客戶端的請求並執行事情 def dealWithClient(newSocket,destAddr):

Qt 執行伺服器與客戶端

文章目錄 思路 伺服器 myserver.h myserver.cpp mythread.h mythread.cpp mysocket.h mysocket.cpp

C網路程式設計--執行伺服器

 伺服器主要用的是socket(雙向的通訊的一端),bind(繫結),listen(切換監聽狀態),accept(與客戶端取得連線) 將accept放入多執行緒,可以多個客戶端連線 #include <stdio.h> //標準輸入輸出 #incl

TCP/IP網路程式設計 基於Linux程式設計_4 --執行伺服器端的實現

執行緒基本概念 前面我們講過多程序伺服器,但我們知道它開銷很大,因此我們才引入執行緒,我們可以把它看成是一種輕量級程序。它相比程序有如下幾個優點: 執行緒的建立和上下文切換開銷更小且速度更快。 執行緒間交換資料時無需特殊技術。 程序:在作業系統構成

linux tcp執行伺服器與客戶端程式設計例項

伺服器端: #include<iostream> #include<arpa/inet.h> #include<sys/socket.h> #include<cstdlib> #include<cstdio> #i

基於非阻塞socket的執行伺服器的實現------一個伺服器如何與個客戶端進行通訊?

      我們首先來看服務端(涉及非阻塞socket和多執行緒): #include <stdio.h> #include <winsock2.h> #include <windows.h> #pragma comment(li

Linux執行伺服器-門禁打卡系統

原始碼地址 系統採用一個伺服器+兩種客戶端(網頁+APP),執行在樹莓派2上 OpenDoorMultiThreadServer OpenDoorMultiThreadServer 實驗室門禁打卡系統 1、mydb是操作資料庫Mysql類,表示每個

執行伺服器模型

本文主要講我個人在多執行緒開發方面的一些粗淺經驗。總結了一兩種常用的執行緒模型,歸納了程序間通訊與執行緒同步的最佳實踐,以期用簡單規範的方式開發多執行緒程式。 文中的“多執行緒伺服器”是指執行在 Linux 作業系統上的獨佔式網路應用程式。硬體平臺為 Int

tcp/ip 執行伺服器端的實現(參考tcp/ip網路程式設計)

執行緒的切換比程序快的多,因為它不需要切換資料區和堆 共享資料區和堆可以用來交換資訊 一、執行緒的建立 pthread_create()函式 #include<pthread.h> int prthread_create(pthread * thread,c

Python TCP 客戶端(配合socket執行伺服器)

''' Python TCP 客戶端(配合socket多執行緒伺服器) by 鄭瑞國 1、建立網路套接字c 2、建立網路連線 3、收發資訊 ''' import socket c = socket.socket() #1、建立網路套接字c c.connect(('127.

Python socket TCP執行伺服器

''' Python socket TCP多執行緒伺服器 by 鄭瑞國 1、建立網路套接字s 2、繫結地址 3、監聽 4、接受客戶端連線 5、多執行緒處理客戶端訊息 ''' import socket import threading s = socket.socket()

java網路程式設計:9、基於TCP的socket程式設計(二)伺服器端迴圈監聽接收個客戶端_執行伺服器程式

宣告:本教程不收取任何費用,歡迎轉載,尊重作者勞動成果,不得用於商業用途,侵權必究!!! 文章目錄 一、核心程式碼編寫 1、伺服器端程式的編寫 2、客戶端程式的編寫 3、測試列印輸出 二、系列文章(java網路程式設計) 上篇講了基於tcp的程式設計的一些基礎知識

【Java】基於TCP協議執行伺服器-客戶端互動控制檯聊天室簡例

      前兩天想到一個手機APP專案,使用到藍芽,發現BluetoothSocket和J2EE網路變成的Socket差不多,使用之餘順手寫一個多執行緒伺服器與客戶端互動實現聊天室的一個小例子,方便新人學習網路程式設計模組,期間使用到多執行緒和IO輸入輸出流的

linux執行伺服器

上一篇文章使用fork函式實現了多程序併發伺服器,但是也提到了一些問題:fork是昂貴的。fork時需要複製父程序的所有資源,包括記憶體映象、描述字等;目前的實現使用了一種寫時拷貝(copy-on-write)技術,可有效避免昂貴的複製問題,但fork仍然是昂貴的;fork子

linux c++執行池的實現(執行伺服器

        本文給出了一個通用的執行緒池框架,該框架將與執行緒執行相關的任務進行了高層次的抽象,使之與具體的執行任務無關。另外該執行緒池具有動態伸縮性,它能根據執行 任務的輕重自動調整執行緒池中執行緒的數量。文章的最後,我們給出一個簡單示例程式,通過該示例程式,我們會發