接上一篇《如何設計一個異步Web服務——接口部分》html
Application已經將任務信息發到了Service服務器中,接下來,Service服務器改如何對自身的資源進行合理分配以知足Application對功能、性能、用戶體驗等各方面的需求呢?服務器
能夠從以下幾個方向入手去考慮:微信
如需轉載,請註明轉自:http://www.cnblogs.com/silenttiger/p/4135461.html多線程
根據上面的要求,咱們會產生以下的設計要求:併發
下面,咱們就根據上述的這些要求開始設計。異步
首先,咱們須要一個http服務器做爲接收Application請求的接口。而後,咱們建立一個QueenAnt(蟻后)類來負責任務和資源的調度,同時還須要若干個WorkerAnt(工蟻)類來處理各個具體的task。性能
注意,這裏的QueenAnt類是靜態的,或者也能夠用單例模式建立。前面提到咱們須要使用線程池(或進程池)技術,因此,在QueenAnt類被實例化之後首先就須要把這個線程池建立出來,並建立若干線程放入這個池中。其中,每一個線程都會實例化一個WorkerAnt類來等待QueenAnt發過來的task。這個地方還有一個問題,那就是咱們的線程池中到底建立幾個線程最優?這個問題我留到後面說明。ui
當這些準備好了之後,Service就能夠等待Application的請求了。spa
當Application向Service發出addTask的請求時,http服務器會將這個請求通知給QueenAnt,並返回QueenAnt返回的taskId。線程
QueenAnt在收到task請求後,除了返回taskId,還須要對這個task的相關信息進行初始化,好比設置task的狀態信息,將task添加到任務隊列等等。
等這些結束之後,QueenAnt就開始針對已經收到的task進行任務調度和資源分配了。咱們定義一個allocateResource方法來處理相關的邏輯。該方法將會指定threadPool中的哪一個具體線程會來處理這個task。這以後,咱們就能夠把task相關的數據發給這個指定的thread進行處理了。而當有task完成時,處理該task的線程中的WorkerAnt就會發送相關信息給QueenAnt,調用QueenAnt的taskEndCallback方法,讓QueenAnt從新分配資源。
當WorkerAnt完成某一個task以後,他須要將這個task的相關信息返回給QueenAnt。同時標記本身爲空閒狀態,以便QueenAnt再進行資源分配。
QueenAnt在收到WorkerAnt關於task完成的消息後,他也須要更新於這個task的相關狀態信息,並在此根據threadPool和taskQueue的具體狀況從新進行資源分配。
到這裏,咱們就經過上圖描述的邏輯,知足了設計要求中的第二和第四點要求。那第一和第三點要求呢,就得經過allocateResource這個方法去實現了。
下面咱們詳細講一下allocateResource這個方法的內部邏輯。
這裏先聲明一下後文的描述方法,咱們把Application發過來的一個任務叫作"task",而把由這個任務拆分出來的許許多多的小任務叫作"子task"。
可能有人會產生疑惑,根據設計要求中的第一點,咱們應該把task拆分爲子task。可上面的設計中,咱們放入taskQueue的倒是Application傳過來的task,是否是差一個拆分的步驟呢?
其實並非這樣,這樣的設計是由於開頭的考慮方向中的第三點和設計要求中第三點,都要求一個task不能夠佔用全部的計算資源。這樣說可能不太好理解,咱們來舉個例子:
首先,Application向Service提交了task01,該task共20個子task,須要Service滿負荷運行5分鐘才能完成。
到第3分鐘的時候,Application又向Service提交了task02,該task共4個子task,須要Service滿負荷運行1分鐘便可完成。
咱們來分析一下這個場景。若是咱們在將task01加入taskQueue以前,就將其拆分爲許多的子task。並把threadPool中的資源依次分給這些子task。那麼到第3分鐘加入task02的各個子task的時候,因爲task01的子task沒有完成,task02只好處於等待狀態。並且須要等task01的幾乎全部子task都完成之後,才能進入處理中的狀態,這一等就是10分鐘。這顯然違背了咱們考慮方向中的第三點和設計要求中第三點。
那麼,怎樣設計這個allocateResource的邏輯才能既知足設計要求中的第一點,又能知足第三點了?個人思路是這樣的。
首先,咱們給task加上兩個屬性threadRequirement和runningThread。threadRequirement表示,爲了完成這個task,若是給其每一個子task分配一個線程,那麼一共須要多少個線程,隨着子task的完成,這個數值會愈來愈小,最後變爲0即表示這個task已經所有完成。runningThread表示,當前有幾個線程正在處理這個task的子task。
而後,allocateResource這個方法有兩個地方會調用,一是當Service收到新的task請求的時候。二是當某個子task完成,QueenAnt中的taskEndCallback被調用的時候。
allocateResource在給task分配資源的時候,應遵照如下幾個準則:
這樣說可能有些抽象。咱們仍是來舉個上面那個例子,假設threadPool中共4個線程,task01的threadRequirement爲20,task02的threadRequirement爲2。過程以下:
大概的邏輯就是上面這樣了。步驟看起來雖然略顯複雜,但其實只有掌握了前面說的4個準則,allocateResource的邏輯仍是很好實現:
至此,關於Service任務調度和資源分配的設計也結束了。
下面,咱們來講一下前面遺留的一個問題:線程池中到底建立幾個線程最優?
爲何最後要特別來談談這個問題,是由於市面上有一種叫作超線程的CPU虛擬化的技術。好比Intel公司的酷睿i3系列CPU,明明是兩個物理核心,在Windows的任務管理中,或在Linux系統的top命令下,顯示的倒是4個核心,由於CPU在硬件層面將兩個物理核心模擬爲4個邏輯核心了。根據咱們上面的設計,天然是但願threadPool中的線程數量越多越好,但是也不能太多。由於多個線程同時爭用一個CPU核心的資源是沒有必要的。因此,若是是4核的CPU,咱們通常會起4個線程放入threadPool。但是在這種使用了超線程技術的CPU平臺上,若是你把線程數目配置爲與CPU邏輯核心數目一致倒是沒有必要的。我在i3平臺上實測數據以下:
線程數 |
總耗時(s) |
CPU 0 使用率 |
CPU 1 使用率 |
CPU 2 使用率 |
CPU 3 使用率 |
1 |
22.4 |
1 |
0 |
98 |
0 |
2 |
12.6 |
2 |
98 |
0 |
97 |
3 |
11.2 |
78 |
64 |
97 |
4 |
4 |
10.5 |
98 |
99 |
100 |
98 |
我想上面的數據已經很好地說明了問題,雖然是4個邏輯核心,雖然你可讓4個線程同時運行,但其實在CPU物理層面,同時運行的指令最多就兩個。也就是4個線程中每兩個線程去爭用一個物理核心的運算資源。
其結果就是,性能上的微小進步卻帶來了CPU使用率的大幅飆升,反而使得用來做爲接口的httpServer響應時間變長。
如需轉載,請註明轉自:http://www.cnblogs.com/silenttiger/p/4135461.html
歡迎關注個人微信公衆號:老虎的小窩