在進行多執行緒程式設計之前我們先解決一個基本問題:什麼是執行緒、什麼是程序,他們之間有什麼區別與聯絡。

(1)程序:執行環境

                  執行緒:執行單位

        用書面一點的知說,程序是一個計算機中程式執行的一個實體,執行緒是作業系統能夠進行運算排程的最小單位。一個執行著的程式就是一個程序,一個程序中至少有一個執行緒正在執行,而實際上程序只是個容器,而本身不具有任何CPU的排程功能,當然也無從談起如何執行。程序為執行緒提供執行的環境,而程序本身只是個抽象的東西,不能正常執行。

(2)同一程序中的多個執行緒

        一個程序也可以包括一個以上的執行緒,這些執行緒都在這個程序的同一個執行環境中執行,包括地址空間、核心物件(包括訊號量、臨界區、檔案控制代碼、Socket等),而從本質上講,就是同一個程序的不同執行緒共享同一個地址空間,所謂的核心物件可以共享是因為這些物件被對映到了這個程序的地址空間中。

        程序與執行緒的關係可以通過一個比喻來理解,程序好比就是一個公司,而執行緒就好比這個公司裡的人。如果要建立一個公司(程序)就必須至少包含一個人,這個人就是法人(主執行緒),而執行緒的多少從理論上講是沒有上限的,但這個公司的資金(記憶體和CPU)是有上限的,不是人越多越好,人越多消耗的資金也就越多,如果入不敷出就會導致這個公司的破產(崩潰)。上一文說到執行緒的個數也一樣的道理,一個公司的不同崗位需要的人的多少首先與公司的業務相關聯,再就跟公司的資金和硬體條件也有關係,不是越多越少,也不是越少越好,而要通過實踐總結出一個合適的人數。

(3)堆與棧

        程序中的記憶體空間除了程式碼、靜態儲存區還有就是動態儲存區,這些區域包括兩大塊:堆(Heap)、棧(Stack)。堆是用於程式進行動態分配時的記憶體區域,而棧是編譯器靜態分配的區域。就是說,堆上到底存什麼,只有在程式執行時才能確定,是程式設計師自己管理的區域,而棧則是編譯時編譯器就已經分配好的儲存空間。堆就是用來存放那些用new(C中用calloc, malloc)建立的物件,而棧用於存入函式呼叫的現場保護、區域性變數。由於堆對於各個執行緒來就是用來存放普通的物件,對於每個執行緒都是一樣的,所以堆可以是全域性的,就是說所有執行緒可以共用一個堆,而棧則不可以,每個執行緒都有獨立的棧。因為棧內不僅可以存普通資料,還需要儲存函式呼叫時的現場保護、引數傳遞,它是有順序的,而堆是無序的。如果一個程序中只有一個執行緒,這種情況是最簡單的,除了固定的程式碼和靜態資料區,其餘的空間可以分配給堆和棧,一個自頂向下增長,一個自底向上增長,只有當二者接上頭的時候才會是記憶體用光的時候。多個執行緒的時候情況就比較複雜,每個執行緒在一開始建立時就必須確定棧的位置和大小,因為棧在同一個地址空間,如果不確定最大值一直漲會導致兩個執行緒的棧重疊,那時程式將無法執行,到底執行緒的棧是多少由程式設計師而定,前提是不要超過程序的空間大小和實體記憶體的大小。

        *所以,在進行多執行緒程式設計時一定要留心你在棧上建立的對你的多少,比如用C語言建立區域性變數陣列時,如果陣列太大就會造成棧溢位,程式就會崩潰。大塊的記憶體使用要在堆上,或定義成全域性變數存放在靜態資料區。

(4)執行緒池

         對於多執行緒程式設計,執行緒池是常用技術之一,這就好比一個公司同一個工種的一個工作組,比方說這個組是客服。隨時都有可能有客戶打電話進來,具體是誰去接這些電話呢,當然是誰空閒誰接,如果都忙的話,再打進來只能排除,真到有一個客戶服務人員空閒下來。為什麼要用執行緒池呢?試想,我們不知道到底同時有多少個客戶會打電話進來,從理論上講可以這樣,沒有電話打進來的時候一個客服都不招,當有電話打進來時再招一下客服,等電話接完了再把他給辭掉,再有電話進來再招、再辭。當然這樣同樣可以完成工作,但是招一個客服人員、辭掉一個客服人員都需要招聘、面試、入職、離職等手續,成本是很高的。與建立、銷燬執行緒的道理是一樣的,建立一個執行緒、銷燬一個執行緒好比招一個客戶服務人員再辭掉一樣成本很高,所以先建立幾個執行緒,有電話就接進來,誰空閒誰接,沒有電話的時候就暫時都休息一下。到底招多少個客服合適呢,這不僅跟記憶體(資金)有關係,還跟CPU(電話)的多少有關係。如果只有3部電話,招4個客服,肯定會有一個沒有位置接電話,如果硬是要讓這4個人在3部電話前不斷換來換去,反而不如3個客服效率更高,因為輪換客服人員(切換CPU)是要浪費電話的利用時間的,所以影響招客服人員多少的三個重要因素就是:業務、資金(記憶體)、電話臺數(CPU數量)。

(5)執行緒同步

        多執行緒程式設計時,最容易出現的一個問題就是資料競爭。比如有兩個執行緒,一個執行緒T1從外部讀取資料放到一塊記憶體中,讀取完成後由另外一個執行緒T2進行處理。如果T2在處理的過程中,剛讀取了一半,而T1又往資料區寫入新的資料,這樣勢必會造成T2處理的資料其實是上一次的一部分和下一次的一部分,處理的結果肯定是錯的,這就是兩個執行緒產生了資料競爭。要解決這些問題就必須通過執行緒同步時間,涉及到使用臨界區、訊號量、互斥量等一些執行緒同步方法,使用不同語言都會有類似的API,稍後再討論。當執行緒與鎖上來之後,又會帶來負作用,那就是“死鎖”,稍後做詳細的討論。

(6)前臺執行緒和後臺執行緒

        在Java和C#中都有前臺執行緒的後臺執行緒之分 ,當所有前臺執行緒都終止程序才會終止,只要有一個前臺執行緒,程序就不會結束,而後臺執行緒則不影響程序的結束,不論有多少個後臺程序的執行,只要有一個前臺程序在執行程序就不會結束。預設都是前臺執行緒,這些執行緒應該都是主要執行緒,只要有一個退出程式就應該退出,如果其它執行緒結束後這個執行緒無論如何也必須結束,則這個執行緒應該設定為後臺執行緒。

(7)執行緒內異常

       記得在所有執行緒內正確處理異常,否則只要有一個執行緒內出現異常,整個程序就會崩潰,所以線上程的過程函式的最外層捕獲所有異常,即使你不能正確處理也要記錄下來現場,知道程式崩潰的時候發生了什麼,便於找出問題所在修復問題。