1. 程式人生 > >後臺開發:核心技術與應用實踐--執行緒與程序間通訊

後臺開發:核心技術與應用實踐--執行緒與程序間通訊

## 多執行緒 程序在多數早期多工作業系統中是執行工作的基本單元。程序是包含程式指令和相關資源的集合,每個程序和其他程序一起參與排程,競爭 CPU 、記憶體等系統資源。每次程序切換,都存在程序資源的儲存和恢復動作,這稱為上下文切換。程序的引入可以解決多使用者支援的問題,但是多程序系統也在如下方面產生了新的問題:程序頻繁切換引起的額外開銷可能會嚴重影響系統性能。 同一個程序內部的多個執行緒,共享的是同一個程序的所有資源。比如,與每個程序獨有自己的記憶體空間不同,同屬一個程序的多個執行緒共享該程序的記憶體空間。通過執行緒可以支援同一個應用程式內部的併發,免去了程序頻繁切換的開銷,另外併發任務間通訊也更簡單。執行緒的切換是輕量級的,所以可以保證足夠快。每當有事件發生狀態改變,都能有執行緒及時響應,而且每次執行緒內部處理的計算強度和複雜度都不大。 一個棧中只有最下方的幀可被讀寫,相應的,也只有該幀對應的那個函式被啟用,處於工作狀態。為了實現多執行緒,則必須繞開棧的限制。為此,在建立一個新的執行緒時,需要為這個執行緒建一個新的棧,每個棧對應一個執行緒,當某個棧執行到全部彈出時,對應執行緒完成任務,並結束。所以,多執行緒的程序在記憶體中有多個棧,多個棧之間以一定的空白區域隔開,以備棧的增長。每個執行緒可呼叫自己棧最下方的幀中的引數和變數,並與其他執行緒共享記憶體中的 Text、heap和global data 區域。要注意的是,對於多執行緒來說,由於同一個程序空間中存在多個棧,任何一個空白區域被填滿都會導致棧溢位 在併發情況下,指令執行的先後順序由核心決定。同一個執行緒內部,指令按照先後順序執行,但不同執行緒之間的指令很難說清楚哪一個會先執行,如果執行的結果依賴於不同執行緒執行的先後的話,那麼就會造成競爭條件,在這樣的狀況下,計算機的結果很難預知,所以 應該儘量避免競爭條件的形成。最常見的解決競爭條件的方法是將原先分離的兩個指令構成不可分割的一個原子操作,而其他任務不能插入到原子操作中。 對於多執行緒程式來說,同步是指在一定的時間內只允許某一個執行緒訪問某個資源。而在此時間內,不允許其他的執行緒訪問該資源,可以通過互斥鎖(mutex) 、條件變數(condition variable)、讀寫鎖(reader-writer lock)和訊號量(emphore)來同步資源。 1. 互斥鎖 > 互斥鎖是一個特殊的變數,它有鎖上(lock)和開啟(unlock)兩個狀態。互斥鎖一般被設定成全域性變數,開啟的互斥鎖可以由某個執行緒獲得。一旦獲得,這個互斥鎖會鎖上,此後只有該執行緒有權開啟,其他想要獲得互斥鎖的執行緒, 會等待直到互斥鎖再次開啟的時候。我們可以將互斥鎖想象成一個只能容納一個人的洗手間, 當某個人進入洗手間的時候,可以從裡面將洗手間鎖上,其他人只能在互斥鎖外面等待那個人出來,才能進去。但在外面等候的人並沒有排隊,誰先看到洗手間了,就可以首先衝進去。 2. 條件變數 > 互斥量是執行緒程式必需的工具,但並非是萬能的。例如,如果執行緒正在等待共享資料內某個條件出現,那會發生什麼呢?它可能重複對互斥物件鎖定和解鎖,每次都會檢查共享資料結構,以查詢某個值。但這是在浪費時間和資源,而且這種繁忙查詢的效率非常低。 > 在每次檢查之間,可以讓呼叫執行緒短暫地進入睡眠,比如睡眠3秒,但是由此執行緒程式碼就無法最快作出響應。真正需要的是這樣一種方法,當執行緒在等待滿足某些條件時使執行緒進入睡眠狀態,一旦條件滿足,就喚醒因等待滿足特定條件而睡眠的執行緒。如果能夠做到這一點,執行緒程式碼將是非常高效的,並且不會佔用寶貴的互斥物件鎖。而這正是條件變數能做的事! > 條件變數通過允許執行緒阻塞和等待另一個執行緒傳送訊號的方法彌補互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變數被用來阻塞一個執行緒,當條件不滿足時,執行緒往往解開相應的互斥鎖並等待條件發生變化。一旦其他的某個執行緒改變了條件變數,它將通知相應的條件變數喚醒一個或多個正被此條件變數阻塞的執行緒,這些執行緒將重新鎖定互斥鎖並重新測試條件是否滿足。 > 條件變數特別適用於多個執行緒等待某個條件的發生。如果不使用條件變數,那麼每個執行緒就需要不斷獲得互斥鎖並檢查條件是否發生,這樣大大浪費了系統的資源。 3. 讀寫鎖 > 對某些資源的訪問會存在兩種可能的情況,一種是訪問必須是排他性的,就是獨佔的意思,這稱作寫操作;另一種情況就是訪問方式可以是共享的,就是說可以有多個執行緒同時去訪問某個資源,這種就稱作讀操作。可以有多個執行緒同時佔用讀模式的讀寫鎖,但是隻能有一個執行緒佔用寫模式的讀寫鎖,讀寫鎖的3種狀態如下所述。 > 1. 當讀寫鎖是寫加鎖狀態時,在這個鎖被解鎖之前,所有試圖對這個鎖加鎖的執行緒都會被阻塞 > 2. 當讀寫鎖在讀加鎖狀態時,所有試圖以讀模式對它進行加鎖的執行緒都可以得到訪問權,但是以寫模式對它進行加鎖的執行緒將會被阻塞 > 3. 當讀寫鎖在讀模式的鎖狀態時,如果有另外的執行緒試圖以寫模式加鎖,讀寫鎖通常會阻塞隨後的讀模式鎖的請求,這樣可以避免讀模式鎖長期佔用,而等待的寫模式鎖請求則長期阻塞。 4. 訊號量 > 訊號量和互斥鎖的區別:互斥鎖只允許一個執行緒進入臨界區,而訊號量允許多個執行緒同時進入臨界區。 **可重入函式** 所謂“可重入函式”,是指可以由多於一個任務併發使用,而不必擔心資料錯誤的函式。相反,“不可函式”則是隻能由一個任務所佔用,除非能確保函式的互斥(或者使用訊號量,或者在程式碼的關鍵部分禁用中斷)。可重入函式可以在任意時刻被中斷,稍後再繼續執行,且不會丟失資料,可重入函式要在使用本地變數或在使用全域性變數時保護自己的資料。 ***可重入函式有以下特點*** 1. 不為連續的呼叫持有靜態資料 2. 不返回指向靜態資料的指標 3. 所有資料都由函式的呼叫者提供 4. 使用本地資料,或者通過製作全域性資料的本地副本來保護全域性資料 5. 如果必須訪問全域性變數,要利用互斥鎖、訊號量等來保護全域性變數 6. 絕不呼叫任何不可重入函式 ***不可重入函式有以下特點*** 1. 函式中使用了靜態變數,無論是全域性靜態變數還是區域性靜態變數 2. 函式返回靜態變數 3. 函式中呼叫了不可重人函式 4. 函式體內使用了靜態的資料結構 5. 函式體內呼叫了 malloc() 或者的 free() 函式 6. 函式體內呼叫了其他標準 I/O 函式 編寫的多執行緒程式,通過定義巨集 _REENTRANT 來告訴編譯器需要可重人功能,這個巨集的定義必須出現於程式中的任何 #include 語句之前,它將為我們做三件事: 1. 它會對部分函式重新定義它們的可安全重入的版本 2. stdio.h 中原來以巨集的形式出現的一些函式將變成可安全重入函式 3. 在 error.h 中定義的變數 error 現在將成為一個函式呼叫,它能夠以一種安全的多執行緒方式來獲取真正的 errno 的值 ## 程序 程序,是計算機中處於執行中程式的實體。以前,程序是最小的執行單位;有了執行緒之後,執行緒成為最小的執行單位,而程序則是執行緒的容器。 程序結構一般由3部分組成:程式碼段、資料段和堆疊段。程式碼段是用於存放程式程式碼的資料,假如機器中有數個程序執行相同的一個程式,那麼它們就可以使用同一個程式碼段。而資料段則存放程式的全域性變數、常量和靜態變數。堆疊段中的棧用於函式呼叫,它存放著函式的引數、函式內部定義的區域性變數。堆疊段還包括了程序控制塊(Process Control Block, PCB), 處於程序核心堆疊的底部,不需要額外分配空間,是程序存在的唯一標識,系統通過PCB的存在而感知程序的存在。系統通過PCB對程序進行管理和排程。PCB包括建立程序、執行程式、退出程序以及改變程序的優先順序等。 程序的建立有兩種方式:一種是由作業系統建立,一種是由父程序建立。 Linux 系統下使用 `fork()` 函式建立一個子程序,其函式原型如下: ```c++ #