天天看點

線程程式設計指南翻譯第一篇(關于線程程式設計)介紹關于線程程式設計

文檔位址

介紹

線程是幾個能使單個程式同時并發執行多個代碼路徑成為可能的技術之一。即使像operation objects和Grand Central Dispatch (GCD) 這些新技術為實作同步并發提供了更加現代化和高效的基礎架構,OS X 和 iOS依然提供接口來建立和管理線程。

關于線程程式設計

多年來,極限計算機的性能在計算機核心上主要受制于單個微型處理器的速度。随着單核處理器達到了實際的限制,晶片制造商進而轉化為多核設計,使計算機有機會同時執行多個任務。盡管 OS X 使用這些核心執行系統相關的任務,我們自己的程式依然可以通過線程使用它們。

什麼是線程

線程是程式内部實作多條執行路徑的一種相對輕量級的方式。在系統層面,程式并行運作,系統根據程式的需要配置設定給每個程式執行時間。然而,在每個程式内部,存在一個或多個執行線程,用于同時或者幾乎同時的方式執行不同任務。系統實際管理這些執行線程,安排它們運作在空閑的核心上,并根據需要搶先中斷它們去允許其他線程運作。

從技術角度來看,線程是一個核心級和應用級資料結構需要去管理執行代碼的組合。核心級結構協調派發事件到線程和優先安排線程到一個有用的核心上。應用級結構包括存儲函數調用的調用棧以及應用程式所需管理和操作的線程資料與狀态的結構。

在非并發的應用程式中,它們隻有一個執行線程。線程以應用程式的主例程以及逐個到實作應用程式整體行為的不同方法和函數的分支開始和結束。相比之下,一個支援多線程的應用程式以一個線程開始,然後根據需要添加更多的執行路徑。每個新路徑有自己的自定義開始例程運作獨立于應用程式主例程的代碼。多線程應用程式提供兩個非常重要的潛在優勢:

  • 多線程可以提高應用程式的感覺響應能力。
  • 多線程可以提高應用程式在多核系統上的實時性能。

如果你的應用程式隻有一個線程,那麼這個線程要幹所有的事。它需要響應事件,更新應用程式視窗,并執行實作應用程式行為的所有計算。問題是,一個線程隻能同時幹一件事情。是以當一個計算需要執行很長一段時間才能完成的時候會發生什麼呢?當你的代碼正在忙碌的計算它所需要的值,你的應用程式就停止了響應使用者事件和更新視窗。如果這種行為經行了很長的時間,使用者可能認為你的應用程式挂起了并且會強制退出。如果把自定義的計算移動到一個分開的線程中,則應用程式的主線程就有空閑及時的響應使用者互動。

随着最近多核計算機的普及,線程提供了一種提高某些類型應用程式性能的方法。執行不同任務的線程可以在不同的處理器核心上同時執行,進而使應用程式可以在給定的時間内增加它的工作量。

當然,線程并不是解決應用程式性能問題的靈丹妙藥。随着線程提供的好處帶來了潛在的問題。在應用程式中具有多個執行路徑會給代碼增加相當大的複雜性。每個線程都必須與其他線程協調其操作,以防止它破壞應用程式的狀态資訊。由于單個應用程式中的線程共享相同的記憶體空間,是以它們可以通路所有相同的資料結構。如果兩個線程試圖同時操作相同的資料結構,一個線程可能覆寫其他線程的更改這意味着破壞了結果資料結構。即使有适當的保護措施,您仍然需要注意編譯器優化,這些優化會在代碼中引入細微(而不是那麼微妙)的錯誤。

線程術語

在深入讨論線程及其支援技術之前,有必要定義一些基本術語。

如果您熟悉UNIX系統,則可能會發現本文檔對“任務”一詞的使用方式不同。在UNIX系統上,術語“任務”有時用于指代正在運作的程序。

本檔案采用以下術語:

  • 術語線程用于指代代碼的單獨執行路徑。
  • 術語程序用于指代正在運作的可執行檔案,其可以包含多個線程。
  • 術語任務用于指代需要執行的抽象工作概念。

線程的代替品

自己建立線程的一個問題是它們會給代碼增加不确定性。線程是一種支援應用程式并發性的相對低級且複雜的方法。如果您不完全了解設計選擇的含義,則可能很容易遇到同步或計時問題,其嚴重性可能從細微的行為更改到應用程式崩潰以及使用者資料損壞。

另一個需要考慮的因素是你是否需要線程或并發。線程解決了如何在同一程序内同時執行多個代碼路徑的特定問題。但是,在某些情況下,您所做的工作量并不能保證并發性。線程在記憶體消耗和CPU時間方面為您的程序帶來了巨大的開銷。您可能會發現此開銷對于預期任務來說太大了,或者其他選項更容易實作。

表1-1列出了一些線程的替代方案。該表包括線程的替換技術(例如Operation objects和GCD)以及旨在有效使用您已有的單線程的替代方法。

表1-1 線程的替代技術

技術 描述
Operation objects 在OS X v10.5中引入的operation object是通常在輔助線程上執行的任務的包裝器。這個包裝器隐藏了執行任務的線程管理方面,讓您可以專注于任務本身。您通常将這些對象與操作隊列對象結合使用,該操作隊列對象實際上管理一個或多個線程上的操作對象的執行。有關如何使用操作對象的更多資訊,請參閱“并發程式設計指南”。
Grand Central Dispatch (GCD) 在Mac OS x v10.6中引入,Grand Central Dispatch是線程的另一種替代方案,可讓您專注于執行所需的任務,而不是線程管理。使用GCD,您可以定義要執行的任務并将其添加到工作隊列,該隊列在适當的線程上處理任務的計劃。工作隊列考慮到可用核心的數量以及目前負載去執行你的任務比起自己使用線程更高效。有關如何使用GCD和工作隊列的資訊,請參閱“并發程式設計指南”
空閑時間通知 對于相對較短且優先級較低的任務,空閑時間通知允許您在應用程式不忙時執行任務。Cocoa使用該NSNotificationQueue對象提供對空閑時間通知的支援。要請求一個空閑時間通知,使用該NSPostWhenIdle選項發送通知到預設的NSNotificationQueue對象。隊列延遲通知對象的傳遞,直到運作循環變為空閑。有關更多資訊,請參閱通知程式設計主題。
異步函數 這些API可能使用系統守護程式和程序或建立自定義線程來執行其任務并将結果傳回給您。(實際的實作是無關緊要的,因為它與代碼分離。)在設計應用程式時,查找提供異步行為的函數,并考慮使用它們而不是在自定義線程上使用等效的同步函數。
計時器 可以在應用程式的主線程上使用計時器來執行定期任務,這些任務太簡單而不需要線程,但仍需要定期維護。有關定時器的資訊,請參閱定時器源。
單獨的程序 盡管比線程更加重量級,但在任務僅與您的應用程式相關的情況下,建立單獨的程序可能很有用。如果任務需要大量記憶體或必須使用root權限執行,則可以使用程序。例如,您可以使用64位伺服器程序計算大型資料集,而32位應用程式将結果顯示給使用者。

注意:當使用fork函數啟動單獨的程序時,你必須總是用一個exec或者一個相似的函數調用跟随fork的調用。依賴于Core Foundation,Cocoa或Core Data架構(顯式或隐式)的應用程式必須對exec函數進行後續調用,否則這些架構可能表現不正常。

線程支援

如果您有現有使用線程的代碼,OS X和iOS提供了幾種在應用程式中建立線程的技術。此外,兩個系統還為管理和同步需要在這些線程上完成的工作提供支援。以下部分描述了在OS X和iOS中使用線程時需要注意的一些關鍵技術。

線程包

雖然線程的底層實作機制是Mach線程,但很少(如果有的話)使用Mach級别的線程。相反,您通常使用更友善的POSIX API或其衍生産品之一。然而,Mach實作确實提供了所有線程的基本功能,包括搶先執行模型和排程線程的能力,是以它們彼此獨立。

清單2-2列出了可以在應用程式中使用的線程技術。

表1-2 線程技術

技術 描述
Cocoa線程 Cocoa使用NSThread類實作線程。Cocoa還提供NSObject了生成新線程和在已經運作的線程上執行代碼的方法。有關更多資訊,請參閱使用NSThread和使用NSObject生成線程。
POSIX線程 POSIX線程提供了一個用于建立線程的基于C的接口。如果您沒有編寫Cocoa應用程式,這是建立線程的最佳選擇。POSIX接口使用起來相對簡單,并為配置線程提供了充分的靈活性。有關更多資訊,請參閱使用POSIX線程
多處理服務 多處理服務是從舊版Mac OS轉換的應用程式使用的基于C的傳統接口。此技術僅适用于OS X,任何新開發都應避免使用。相反,您應該使用NSThread類或POSIX線程。如果需要有關此技術的更多資訊,請參閱“ 多處理服務程式設計指南”。

在應用程式級别,所有線程的行為方式與其他平台上的行為基本相同。啟動線程後,線程以三種主要狀态之一運作:running,ready或blocked。如果一個線程目前沒有運作,它将被阻塞并等待輸入,或者它已準備好運作但尚未安排執行此操作。線程繼續在這些狀态之間來回移動,直到它最終退出并移動到終止狀态。

建立新線程時,必須為該線程指定入口點函數(或Cocoa線程的入口點方法)。此入口點函數構成您要線上程上運作的代碼。當函數傳回時,或者顯式終止線程時,線程将永久停止并由系統回收。由于線程在記憶體和時間方面的建立成本相對較高,是以建議您的入口點函數執行大量工作或設定運作循環以允許執行重複工作。

有關可用線程技術及其使用方法的更多資訊,請參閱線程管理。

Run Loops

運作循環是一個基礎結構,用于管理線上程上異步到達的事件。運作循環通過監視線程的一個或多個事件源來工作。當事件到達時,系統喚醒線程并将事件排程到運作循環,然後運作循環将它們分派給您指定的處理程式。如果沒有事件存在和準備需處理,則運作循環使線程進入休眠狀态。

您不需要對您建立的任何線程使用運作循環,但這樣做可以為使用者提供更好的體驗。運作循環可以建立使用最少量資源的長期線程。因為運作循環在沒有任何操作時将其線程置于休眠狀态,是以它消除了輪詢的需要,這會浪費CPU周期并阻止處理器本身休眠并節省電力。

要配置運作循環,您所要做的就是啟動線程,擷取對運作循環對象的引用,安裝事件處理程式,并告訴運作循環運作。OS X提供的基礎結構會自動為您處理主線程的運作循環的配置。但是,如果您計劃建立長期存在的輔助線程,則必須自己為這些線程配置運作循環。

中提供了有關運作循環以及如何使用它們的例子詳細運作循環。

同步工具

線程程式設計的一個危險是多線程之間的資源争用。如果多個線程嘗試同時使用或修改同一資源,則可能會出現問題。緩解該問題的一種方法是完全消除共享資源,并確定每個線程都有自己獨特的資源集來操作。但是,維護完全獨立的資源不是一種好的選擇,您可能使用鎖,條件,原子操作和其他技術來同步對資源的通路。

鎖為代碼提供強制形式的保護,一次隻能由一個線程執行。最常見的鎖定類型是互斥鎖,也稱為互斥鎖。當一個線程試圖擷取目前由另一個線程持有的互斥鎖時,它會阻塞,直到另一個線程釋放該鎖。多個系統架構為互斥鎖提供支援,盡管它們都基于相同的底層技術。此外,Cocoa提供了互斥鎖的幾種變體,以支援不同類型的行為,例如遞歸。有關可用鎖類型的更多資訊,請參閱鎖。

除鎖定外,系統還提供對條件的支援,以確定應用程式中任務的正确排序。條件充當守門人,阻止給定線程,直到它表示的條件變為真。當發生這種情況時,條件會釋放線程并允許它繼續。POSIX層和Foundation架構都為條件提供直接支援。(如果使用操作對象,則可以配置操作對象之間的依賴關系以對任務的執行進行排序,這與條件提供的行為非常相似。)

雖然鎖和條件在并發設計中非常常見,但原子操作是保護和同步資料通路的另一種方法。在可以對标量資料類型執行數學或邏輯運算的情況下,原子操作為鎖定提供了輕量級替代。原子操作使用特殊的硬體指令來確定在其他線程有機會通路變量之前完成對變量的修改。

有關可用同步工具的詳細資訊,請參閱同步工具。

線程間通信

雖然良好的設計可以最大限度地減少所需的通信量,但在某些時候,線程之間的通信變得必要。(線程的工作是為您的應用程式工作,但如果從未使用過該作業的結果,它有什麼用處?)線程可能需要處理新的作業請求或将其進度報告給應用程式的主線程。在這些情況下,您需要一種方法來從一個線程擷取資訊到另一個線程。幸運的是,線程共享相同的程序空間這一事實意味着您有很多通信選項。

線程之間有許多通信方式,每種方式都有自己的優點和缺點。配置線程局部存儲列出了可以在OS X中使用的最常見的通信機制。(除了消息隊列和Cocoa分布式對象,這些技術在iOS中也可用。)此表中的技術按複雜度遞增。

表1-3 通信機制

header 1 header 2
直接消息傳遞 Cocoa應用程式支援直接在其他線程上執行選擇器的能力。此功能意味着一個線程基本上可以在任何其他線程上執行方法。因為它們是在目标線程的上下文中執行的,是以以這種方式發送的消息會在該線程上自動序列化。有關輸入源的資訊,請參閱Cocoa執行選擇器源。
全局變量,共享記憶體和對象 在兩個線程之間傳遞資訊的另一種簡單方法是使用全局變量,共享對象或共享記憶體塊。雖然共享變量快速而簡單,但它們也比直接消息傳遞更脆弱。必須使用鎖或其他同步機制小心保護共享變量,以確定代碼的正确性。如果不這樣做可能會導緻競争條件,資料損壞或崩潰。
條件 條件是一種同步工具,可用于控制線程何時執行特定代碼部分。您可以将條件視為門衛,讓線程僅在滿足所述條件時運作。有關如何使用條件的資訊,請參閱使用條件。
運作循環源 自定義運作循環源是您設定為線上程上接收特定于應用程式的消息的源。因為它們是事件驅動的,是以當沒有任何事情要做時,運作循環源會讓你的線程自動進入休眠狀态,進而提高線程的效率。有關運作循環和運作循環源的資訊,請參閱運作循環。
端口和套接字 基于端口的通信是兩種線程之間通信的更精細的方式,但它也是一種非常可靠的技術。更重要的是,端口和套接字可用于與外部實體(例如其他程序和服務)進行通信。為了提高效率,端口是使用運作循環源實作的,是以當端口上沒有資料等待時,線程會休眠。有關運作循環和基于端口的輸入源的資訊,請參閱運作循環。
消息隊列 傳統的多處理服務定義了用于管理傳入和傳出資料的先進先出(FIFO)隊列抽象。盡管消息隊列簡單友善,但它們并不像其他一些通信技術那樣高效。有關如何使用消息隊列的詳細資訊,請參閱“多處理服務程式設計指南”。
Cocoa分布式對象 分布式對象是一種Cocoa技術,可提供基于端口的通信的進階實作。盡管可以将此技術用于線程間通信,但由于其産生的開銷量很大,是以非常不鼓勵這樣做。分布式對象更适合與其他程序通信,其中程序之間的開銷已經很高。有關更多資訊,請參閱分布式對象程式設計主題。

設計技巧

以下部分提供了一些指導原則,可幫助您以確定代碼正确性的方式實作線程。其實一些指南還為自己的線程代碼提供實作更好性能的技巧。與任何性能技巧一樣,您應始終在更改代碼之前,期間和之後收集相關的性能統計資訊。

避免明确的建立線程

編寫線程建立代碼很繁瑣且可能容易出錯是以要盡量避免。OS X和iOS通過其他API提供對并發的隐式支援。比起建立自己的線程,建議應用異步APIs,GCD或者operation objects來工作。這些技術為您做幕後的線程相關工作,并保證正确執行。此外,通過根據目前系統負載調整活動線程數,GCD和operation objects等技術可以比您自己的代碼更有效地管理線程。有關GCD和operation objects的更多資訊,請參閱“并發程式設計指南”。

保持線程合理的忙碌

如果您決定手動建立和管理線程,請記住線程占用寶貴的系統資源。您應該盡力確定配置設定給線程的任何任務都是合理的活躍和高效的。與此同時,您不應該害怕終止花費大部分時間閑置的線程。線程使用大量記憶體,其中一些是有線的,是以釋放空閑線程不僅有助于減少應用程式的記憶體占用,還可以釋放更多實體記憶體供其他系統程序使用。

要點: 在開始終止空閑線程之前,應始終記錄應用程式目前性能的一組基線度量。嘗試更改後,請進行其他測量以驗證更改是否實際上提高了性能,而不是損壞它。

避免共享資料結構

避免與線程相關的資源沖突的最簡便和最簡單的方法是為程式中的每個線程提供它所需的任何資料的副本。當您最小化線程之間的通信和資源争用時,并行代碼最有效。

建立多線程應用程式很難。即使您非常小心并在代碼中的所有正确接合點處鎖定共享資料結構,您的代碼仍可能在語義上不安全。例如,如果希望以特定順序修改共享資料結構,則代碼可能會遇到問題。将代碼更改為基于事務的模型以進行補償可能随後會抵消具有多個線程的性能優勢。首先消除資源争用通常會導緻設計更簡單,性能更佳。

線程與使用者界面

如果您的應用程式具有圖形使用者界面,建議您從應用程式的主線程接收與使用者相關的事件并啟動界面更新。此方法有助于避免與處理使用者事件和繪制視窗内容相關的同步問題。某些架構(如Cocoa)通常需要此行為,但即使對于那些不這樣做的架構,将此行為保留在主線程上也具有簡化管理使用者界面的邏輯的優勢。

有一些例外值得注意,從其他線程執行圖形操作是有利的。例如,您可以使用輔助線程來建立和處理圖像,并執行其他與圖像相關的計算。使用輔助線程進行這些操作可以大大提高性能。如果您不确定特定的圖形操作,請計劃從主線程執行此操作

有關使用Cocoa線程安全的更多資訊,請參閱線程安全摘要。有關使用Cocoa繪圖的更多資訊,請參閱“ Cocoa繪圖指南”。

退出時留意線程的行為

程序将一直運作,直到所有未分離的線程都退出。預設情況下,隻将應用程式的主線程建立為非分離,但您也可以建立其他線程。當使用者退出應用程式時,通常認為立即終止所有分離的線程是合适的行為,因為分離線程完成的工作被認為是可選的。但是,如果您的應用程式使用背景線程将資料儲存到磁盤或執行其他關鍵工作,您可能希望将這些線程建立為非分離,以防止在應用程式退出時丢失資料。

将線程建立為非分離(也稱為可連接配接)需要您做額外的工作。由于大多數進階線程技術預設情況下不建立可連接配接線程,是以您可能必須使用POSIX API來建立線程。此外,您必須在應用程式的主線程中添加代碼,以便在最終退出時與非分離線程連接配接。有關建立可連接配接線程的資訊,請參閱設定線程的分離狀态。

如果您正在編寫可可應用程式,您還可以使用applicationShouldTerminate:委托方法将應用程式的終止延遲到以後的時間或完全取消它。延遲終止時,您的應用程式需要等待任何關鍵線程完成其任務,然後調用該replyToApplicationShouldTerminate:方法。有關這些方法的詳細資訊,請參閱NSApplication類參考。

處理特殊情況

異常處理機制依賴于目前調用堆棧在抛出異常時執行任何必要的清理。因為每個線程都有自己的調用堆棧,是以每個線程都負責捕獲自己的異常。未能在輔助線程中捕獲異常與未在主線程中捕獲異常相同:程序會終止。不能将未捕獲的異常抛出到其他線程進行處理。

如果在目前線程捕獲到異常需要通知其他線程(如主線程)。你應該捕獲異常并簡單地向另一個線程發送一條消息,指出發生了什麼。根據您的模型和您要執行的操作,捕獲異常的線程可以繼續處理(如果可能),等待指令,或者隻是退出。

注意: 在Cocoa中,NSException對象是一個自包含的對象,能在被捕獲時從一個線程傳遞給另一個線程。

在某些情況下,可能會自動為您建立異常處理程式。例如,@synchronizedObjective-C中的指令包含一個隐式異常處理程式。

幹淨的終止線程

線程退出的最佳方法當然是讓它到達主入口點例程的末尾。雖然有立即終止線程的函數,但這些函數應該僅作為最後的手段使用。線上程到達其自然終點之前終止線程阻礙了線程自行清理。如果線程已配置設定記憶體,打開檔案或擷取其他類型的資源,則代碼可能無法回收這些資源,進而導緻記憶體洩漏或其他潛在問題。

有關退出線程的正确方法的更多資訊,參閱請終止線程。

代碼庫的線程安全

雖然應用程式開發人員可以控制應用程式是否使用多個線程執行,但庫開發人員卻沒有。在開發庫時,您必須假設調用應用程式是多線程的,或者可以随時切換為多線程。是以,您應始終對代碼的關鍵部分使用鎖。

對于庫開發人員,僅在應用程式變為多線程時才建立鎖是不明智的。如果您需要在某個時刻鎖定代碼,請在使用庫的早期建立鎖定對象,最好是在某種初始化庫時顯式調用。雖然您也可以使用靜态庫初始化函數來建立此類鎖,但隻有在沒有其他方法時才嘗試這樣做。執行初始化函數會增加加載庫所需的時間,并可能對性能産生負面影響。

注意:始終記得在庫代碼中平衡lock和unlock互斥鎖的調用。還應該記住鎖定庫中資料結構,而不是依賴調用代碼來提供線程安全的環境。

如果您正在開發Cocoa庫,則可以為NSWillBecomeMultiThreadedNotification注冊觀察者,以便在應用程式變為多線程時通知您。但是,您不應該依賴于接收此通知,因為它可能會在調用庫代碼之前排程線程。