用多線程分解任務進行並發處理
1. 從單線程任務到多線程任務的轉換 本章我們將處理兩種類型的資料,一種是IO密集型任務,另一中是計算密集型任務。
分而治之 如果我們有數百隻股票需要處理,你可以一隻只股票地線性處理,不過那可能是一種很愚蠢的行為。為了使我們的程式能夠更快的運行,我們可以把這個任務分成多個任務並行地處理。不過我們也不能分成太多個線程,因為電腦的資源是有限的,開闢線程會消耗額外的線程資源。
決定線程數量 對於一個大型程式,我們可以開闢的線程數量至少等於運行機器的cpu核心數量。java程式裡我們可以通過下面的一行代碼得到這個數量:
Runtime.getRuntime().availableProcessors();
所以最小線程數量即時cpu核心數量。如果所有的任務都是計算密集型的,這個最小線程數量就是我們需要的線程數。開闢更多的線程只會影響程式的效能,因為線程之間的切換工作,會消耗額外的資源。如果任務是IO密集型的任務,我們可以開闢更多的線程執行任務。當一個任務執行IO操作的時候,線程將會被阻塞,處理器立刻會切換到另外一個合適的線程去執行。如果我們只擁有與核心數量一樣多的線程,即使我們有任務要執行,他們也不能執行,因為處理器沒有可以用來調度的線程。 如果線程有50%的時間被阻塞,線程的數量就應該是核心數量的2倍。如果更少的比例被阻塞,那麼它們就是計算密集型的,則需要開闢較少的線程。如果有更多的時間被阻塞,那麼就是IO密集型的程式,則可以開闢更多的線程。於是我們可以得到下面的線程數量計算公式: 線程數量= 核心數量 / (1 - 阻塞率) 我們可以通過相應的分析工具或者java的management包來得到阻塞率的數值。
決定分隔的任務數量 我們已經知道了如何決定線程數量,現在我們來討論下把任務分隔成多少子任務是最合適的,每個子任務並發執行。於是,我們第一個想到的是,分配與線程數量一樣多的子任務是最合適的,這看起來很合適但其實是不夠的,我們忽略了具體子任務的性質。 例如,在股票處理的程式上,把分成與線程數量一樣多的子任務就足夠了。 但是對於,擷取素數的程式,這樣就是有問題的。因為偶數是很容易就處理完,大的素數比小的素數會花費更多的時間處理。把所有的數根據從大到小分隔成與線程數量一樣多的部分進行處理並不能幫我們提升多少效能。一些線程會比另外一些線程更快的執行完,這樣的話核心就不能有效利用。 換言之,如果我們想這樣分隔,我們必須花費很多精力去恰當的分隔這些資料到這些任務來處理,以至於這些任務都能有同等的負載平衡。然而這樣做的話就會存在兩個問題:1,這樣的分隔的確很難。2,這種分隔在程式裡面也比較複雜。 事實證明,讓每個cpu核心保持忙碌比想辦法讓每個部分的負擔一樣更加有效。在處理器的角度來看,當仍然有任務需要處理的時候,沒有處理器空閑著。所以,與其想辦法把任務分成負載相等的子任務,不如分隔比線程數量更多的線程讓處理器保持忙碌。
2. 擷取高效並發效能的方法 一方面,我們要確保並發的一致性和準確性;另一方面,我們要確保在給定的機器上得到更好的效能。下面,我們來探尋滿足這兩者的方法。 只要我們能完全的消除共用可變狀態變數,就可以很容易地避免條件競爭或者一致性問題。當多個線程不競爭地去獲得可變資料,變數就不會有可變性問題。我們也不用擔心控制線程的執行順序。 如果可能的話,就提供多個線程共用的不可變變數。否則,遵循孤立可變性原則,確保只有一個線程能夠訪問這個可變變數。我們現在不討論同步線程狀態的情況,我們暫時只確保只有一個線程可以訪問可變的變數。 我們建立多少線程並且將任務分成多少份子任務將會影響並發程式的效能。首先,為了利用並發性所帶來的好處,我們必須能夠將任務劃分成子任務。如果一個問題有一個不可被分割的重要部分,那麼這個程式不能通過引入並發來獲得更好的效能。如果任務是IO密集型的,或者有大量的IO操作,那麼開闢更多的線程將會帶來效能上的提高。在這種情況下,開闢比cpu核心更多的線程將會對程式的效能很有益處。對於一個計算密集型的任務,開闢比cpu核心多的線程其實並不利於效能的提高。然而,我們至少可以通過開闢與核心數量相等的效能來提高效能。 雖然開闢線程可以影響效能,但是這不是唯一的一種情況。通過分解任務,也可以影響效能。