1. 程式人生 > >多執行緒伺服器模型

多執行緒伺服器模型

本文主要講我個人在多執行緒開發方面的一些粗淺經驗。總結了一兩種常用的執行緒模型,歸納了程序間通訊與執行緒同步的最佳實踐,以期用簡單規範的方式開發多執行緒程式。

文中的“多執行緒伺服器”是指執行在 Linux 作業系統上的獨佔式網路應用程式。硬體平臺為 Intel x64 系列的多核 CPU,單路或雙路 SMP 伺服器(每臺機器一共擁有四個核或八個核,十幾 GB 記憶體),機器之間用百兆或千兆乙太網連線。這大概是目前民用 PC 伺服器的主流配置。

本文不涉及 Windows 系統,不涉及人機互動介面(無論命令列或圖形);不考慮檔案讀寫(往磁碟寫 log 除外),不考慮資料庫操作,不考慮 Web 應用;不考慮低端的單核主機或嵌入式系統,不考慮手持式裝置,不考慮專門的網路裝置,不考慮高階的 >=32 核 Unix 主機;只考慮 TCP,不考慮 UDP,也不考慮除了區域網絡之外的其他資料收發方式(例如串並口、USB口、資料採集板卡、實時控制等)。

有了以上這麼多限制,那麼我將要談的“網路應用程式”的基本功能可以歸納為“收到資料,算一算,再發出去”。在這個簡化了的模型裡,似乎看不出用多執行緒的必要,單執行緒應該也能做得很好。“為什麼需要寫多執行緒程式”這個問題容易引發口水戰,我放到另一篇部落格裡討論。請允許我先假定“多執行緒程式設計”這一背景。

“伺服器”這個詞有時指程式,有時指程序,有時指硬體(無論虛擬的或真實的),請注意按上下文區分。另外,本文不考慮虛擬化的場景,當我說“兩個程序不在同一臺機器上”,指的是邏輯上不在同一個作業系統裡執行,雖然物理上可能位於同一機器虛擬出來的兩臺“虛擬機器”上。

本文假定讀者已經有多執行緒程式設計的知識與經驗,這不是一篇入門教程。

本文承蒙 Milo Yip 先生審讀,在此深表謝意。當然,文中任何錯誤責任均在我。

目  錄

執行緒池 4

歸納 5

歸納 15

7 總結 15

1 程序與執行緒

“程序/process”是操作裡最重要的兩個概念之一(另一個是檔案),粗略地講,一個程序是“記憶體中正在執行的程式”。本文的程序指的是 Linux 作業系統通過 fork() 系統呼叫產生的那個東西,或者 Windows 下 CreateProcess() 的產物,不是 Erlang 裡的那種輕量級程序。

每個程序有自己獨立的地址空間 (address space),“在同一個程序”還是“不在同一個程序”是系統功能劃分的重要決策點。Erlang 書把“程序”比喻為“人”,我覺得十分精當,為我們提供了一個思考的框架。

每個人有自己的記憶 (memory),人與人通過談話(訊息傳遞)來交流,談話既可以是面談(同一臺伺服器),也可以在電話裡談(不同的伺服器,有網路通訊)。面談和電話談的區別在於,面談可以立即知道對方死否死了(crash, SIGCHLD),而電話談只能通過週期性的心跳來判斷對方是否還活著。

有了這些比喻,設計分散式系統時可以採取“角色扮演”,團隊裡的幾個人各自扮演一個程序,人的角色由程序的程式碼決定(管登陸的、管訊息分發的、管買賣的等等)。每個人有自己的記憶,但不知道別人的記憶,要想知道別人的看法,只能通過交談。(暫不考慮共享記憶體這種 IPC。)然後就可以思考容錯(萬一有人突然死了)、擴容(新人中途加進來)、負載均衡(把 a 的活兒挪給 b 做)、退休(a 要修復 bug,先別給他派新活兒,等他做完手上的事情就把他重啟)等等各種場景,十分便利。

“執行緒”這個概念大概是在 1993 年以後才慢慢流行起來的,距今不過十餘年,比不得有 40 年光輝歷史的 Unix 作業系統。執行緒的出現給 Unix 添了不少亂,很多 C 庫函式(strtok(), ctime())不是執行緒安全的,需要重新定義;signal 的語意也大為複雜化。據我所知,最早支援多執行緒程式設計的(民用)作業系統是 Solaris 2.2 和 Windows NT 3.1,它們均釋出於 1993 年。隨後在 1995 年,POSIX threads 標準確立。

執行緒的特點是共享地址空間,從而可以高效地共享資料。一臺機器上的多個程序能高效地共享程式碼段(作業系統可以對映為同樣的實體記憶體),但不能共享資料。如果多個程序大量共享記憶體,等於是把多程序程式當成多執行緒來寫,掩耳盜鈴。

“多執行緒”的價值,我認為是為了更好地發揮對稱多路處理 (SMP) 的效能。在 SMP 之前,多執行緒沒有多大價值。Alan Cox 說過 A computer is a state machine. Threads are for people who can't program state machines. (計算機是一臺狀態機。執行緒是給那些不能編寫狀態機程式的人準備的。)如果只有一個執行單元,一個 CPU,那麼確實如 Alan Cox 所說,按狀態機的思路去寫程式是最高效的,這正好也是下一節展示的程式設計模型。

2 典型的單執行緒伺服器程式設計模型

UNP3e 對此有很好的總結(第 6 章:IO 模型,第 30 章:客戶端/伺服器設計正規化),這裡不再贅述。據我瞭解,在高效能的網路程式中,使用得最為廣泛的恐怕要數“non-blocking IO + IO multiplexing”這種模型,即 Reactor 模式,我知道的有:

  • lighttpd,單執行緒伺服器。(nginx 估計與之類似,待查)
  • libevent/libev
  • ACE,Poco C++ libraries(QT 待查)
  • Java NIO (Selector/SelectableChannel), Apache Mina, Netty (Java)
  • POE (Perl)
  • Twisted (Python)

相反,boost::asio 和 Windows I/O Completion Ports 實現了 Proactor 模式,應用面似乎要窄一些。當然,ACE 也實現了 Proactor 模式,不表。

在“non-blocking IO + IO multiplexing”這種模型下,程式的基本結構是一個事件迴圈 (event loop):(程式碼僅為示意,沒有完整考慮各種情況)

?
1 2 3 4 5 6 7 8 9 10 11 12 13 while (!done) { int timeout_ms = max(1000, getNextTimedCallback()); int retval = ::poll(fds, nfds, timeout_ms); if (retval < 0) { 處理錯誤 }else { 處理到期的 timers if (retval > 0) { 處理 IO 事件 } } }

當然,select(2)/poll(2) 有很多不足,Linux 下可替換為 epoll,其他作業系統也有對應的高效能替代品(搜 c10k problem)。

Reactor 模型的優點很明顯,程式設計簡單,效率也不錯。不僅網路讀寫可以用,連線的建立(connect/accept)甚至 DNS 解析都可以用非阻塞方式進行,以提高併發度和吞吐量 (throughput)。對於 IO 密集的應用是個不錯的選擇,Lighttpd 即是這樣,它內部的 fdevent 結構十分精妙,值得學習。(這裡且不考慮用阻塞 IO 這種次優的方案。)

當然,實現一個優質的 Reactor 不是那麼容易,我也沒有用過坊間開源的庫,這裡就不推薦了。

3 典型的多執行緒伺服器的執行緒模型

這方面我能找到的文獻不多,大概有這麼幾種:

1. 每個請求建立一個執行緒,使用阻塞式 IO 操作。在 Java 1.4 引入 NIO 之前,這是 Java 網路程式設計的推薦做法。可惜伸縮性不佳。

2. 使用執行緒池,同樣使用阻塞式 IO 操作。與 1 相比,這是提高效能的措施。

3. 使用 non-blocking IO + IO multiplexing。即 Java NIO 的方式。

4. Leader/Follower 等高階模式

One loop per thread

此種模型下,程式裡的每個 IO 執行緒有一個 event loop (或者叫 Reactor),用於處理讀寫和定時事件(無論週期性的還是單次的),程式碼框架跟第 2 節一樣。

這種方式的好處是:

  • 執行緒數目基本固定,可以在程式啟動的時候設定,不會頻繁建立與銷燬。
  • 可以很方便地線上程間調配負載。

event loop 代表了執行緒的主迴圈,需要讓哪個執行緒幹活,就把 timer 或 IO channel (TCP connection) 註冊到那個執行緒的 loop 裡即可。對實時性有要求的 connection 可以單獨用一個執行緒;資料量大的 connection 可以獨佔一個執行緒,並把資料處理任務分攤到另幾個執行緒中;其他次要的輔助性 connections 可以共享一個執行緒。

對於 non-trivial 的服務端程式,一般會採用 non-blocking IO + IO multiplexing,每個 connection/acceptor 都會註冊到某個 Reactor 上,程式裡有多個 Reactor,每個執行緒至多有一個 Reactor。

多執行緒程式對 Reactor 提出了更高的要求,那就是“執行緒安全”。要允許一個執行緒往別的執行緒的 loop 裡塞東西,這個 loop 必須得是執行緒安全的。

執行緒池

不過,對於沒有 IO 光有計算任務的執行緒,使用 event loop 有點浪費,我會用有一種補充方案,即用 blocking queue 實現的任務佇列(TaskQueue):

?
1 2 3 4 5 6 7 8 9 blocking_queue<boost::function<void()> > taskQueue;  // 執行緒安全的阻塞佇列 void worker_thread() { while (!quit) { boost::function<void()> task = taskQueue.take();  // this blocks

相關推薦

執行伺服器模型

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

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

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

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

java執行-記憶體模型

併發處理的廣泛應用是使得amdahl定律代替摩爾定律成為計算機效能發展源動力的根本原因,是人類壓榨計算機運算能力的最有力武器。 上一篇《java 多執行緒—執行緒怎麼來的 》中我們瞭解了執行緒在作業系統中的是如何派生出來的,這一篇我們聊聊jvm的記憶體模型,瞭解一些jvm在

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類,表示每個

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++執行池的實現(執行伺服器

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

TCP執行伺服器

兩種版本分別應用兩種不同的子執行緒實現方式 版本一 import socket import threading def func(tcp_socket): while True: # 接收資訊 cilent_s