天天看點

iOS開發之多線程(理論知識)

1. 關于多線程程式設計 

多年以來,計算機的性能在很大程度上被單核處理器的速度所限制。在目前技術下,單核處理器的速度已經到達某種極限,是以,晶片制造商們轉而專注于多核設計,以使計算機可以同時執行多個任務。Mac OS X 可以利用多核計算,更好的執行系統相關的任務。而開發人員也可以通過線程提高自己程式的性能。 

1) 什麼是線程? 

線程是在程式内部運作多個流程的輕量機關。在系統級别上,程式使用系統提供的執行時間,各自運作。然而,在每個程式内部,存在着一個或多個被執行的線程,這些線程可以在同一時刻(或将近同時)完成不同的任務。由作業系統本身管理這些線程的執行,例如為線程配置設定執行時間和執行的核心,或是中斷線程來使其他線程有機會執行。 

從技術角度來看,線程是“核心級”和“程式級”資料結構的集合,用來管理代碼的執行。核心級的資料結構主要處理針對線程的事件,安排線程在可用的核心上執行。而程式級的結構包括調用棧(可以儲存函數調用),還有用來管理和計算線程屬性和狀态的資料。 

在非并行程式中,隻有一個線程的執行。線程起始于main函數,結束于main函數。在main函數内部,程式一句一句的執行。相比較而言,支援并行的程式起始于一個線程,然後在添加其他線程,進而建立了額外的執行路徑。每個這樣的路徑都擁有自己的執行過程,而和程式中的主線程并行不悖。在程式中加入多線程,會為你帶來以下兩個非常重要的好處: 

1. 多線程可以提高程式的反應速度。 

2. 在多核系統上,多線程可以增強程式的實時處理能力。 

如果你的程式隻有一個線程,這一個線程就得做所有事情。它必須響應事件、更新程式視窗、執行所有的運算工作。單線程的問題是在特定時刻内,它隻能處理一件事。那麼如果我們的某些計算工作需要耗費很長時間,會發生什麼?當代碼忙于計算需要的值時,程式就停止對使用者事件的響應,也不會再對視窗進行更新。如果時間很長的話,使用者可能以為程式卡住了,強行關閉程式。但是,如果你将需要的計算工作放到一個獨立的線程中去,你的主線程就有足夠的時間去響應使用者事件。 

在多核電腦日益普及的今天,線程提高了某些程式的性能。執行多個任務的線程可以使用不同的處理器核心,進而使增加給定時間内完成的工作量成為可能。 

但是,線程并不是改善程式性能問題的萬金油。伴随着線程提供的好處,更多的潛在問題也随之産生。在程式中加入多個執行路徑會大大增加代碼的複雜度。每個線程都需要協調各自的行為,來防止弄亂程式的狀态。這是因為位于同一程式的所有線程共享同一個記憶體空間,它們對所有的這些資料結構都有通路權限。如果兩個線程同時計算一份資料,其中一個線程可能将另外線程修改的内容覆寫,那麼計算結果就亂套了。即使是使用了恰當的保護方式,我們還是必須注意編譯器優化選項(它可能導緻代碼出現很微妙的錯誤)。 

2)線程相關術語 

在深入研究線程和相關技術之前,必須先了解一些基本術語的定義。 

如果你熟悉Carbon架構中的多核處理服務接口(Multiprocessor Services API),或是熟悉Unix系統,你将會發現術語“任務”(task)在本文檔中的含義略有不同。在Mac OS的早期版本中,術語“任務”被用來差別使用Multiprocessor Services建立的和使用Carbon Thread Manager建立的線程。而在Unix系統之上,術語“任務”有時指的是運作中的程序(process)。實際情況中,一個Multiprocessor Services的任務和一個搶占式線程是一樣的(注:沒有深入的用過Carbon,有不妥之處還望指正)。 

考慮到Carbon Thread Manager和Multiprocessor Services的API都是在Mac OS系統上的遺留技術,本文檔遵循以下的術語規範: 

1. 術語線程(thread)指獨立的代碼執行路徑。 

2. 術語程序(process)指程式的運作,程序可以包括多個線程。 

3. 術語任務(task)指的是一份可以被執行的工作,是個抽象概念。 

3)線程備選方案 

自己建立線程時的一個問題是:它們為你的代碼增加了不确定因素。線程是讓程式支援并行操作的一種相對低層次的方式,而且比較複雜。如果你沒有完全了解自己針對多線成設計的初衷,就很容易遇到同步或時間控制的問題。輕者改變程式的行為,嚴重的會造成程式崩潰或是弄亂使用者的資料。 

另一個需要考慮的因素是:你是否真的需要并行操作?線程可以解決在同一程序中同時執行多段代碼的問題。在某些情況下,一些工作并不能保證被同時執行。線程可能帶來更多的負荷,有記憶體消耗方面的,抑或是占用CPU時間。你需要研究是否值得為某個任務承擔這樣的負荷,或者使用其他簡單的執行方式。 

Table 1-1 列出了可供選擇的線程實作方式。表中包含了線程的替代技術(操作對象-- operation object 和Grand central Dispatch),還包含了旨在提高效率的單線程技術。 

Table 1-1  Alternative technologies to threads 

Technology   Description 

Operation objects| Mac OS X v10.5後被引入,一個操作對象是一項任務的封裝,可以被子線程們來執行。使用這樣的封裝技術,可以忽略線程管理方面的麻煩,使程式員專注于任務本身。通常情況下,我們搭配使用操作隊列對象(operation queue object)來使用操作對象。操作隊列對象會管理操作對象在多線程中的執行。具體内容參考Concurrency Programming Guide. 

Grand Central Dispatch |Mac OS X v10.6後被引入,Grand Central Dispatch是讓我們專注于任務本身而非線程管理的另外的選擇。使用Grand Central Dispatch,我們定義一項任務,并将其加入到一個工作隊列中,隊列會負責在合适的線程中執行任務。隊列還會檢視可用核心的數量和目前任務負載,進而比自己使用線程更高效的執行任務。 

Idle-time notifications |對于那些相對來說較小的、優先級低的任務,“空閑時間通知”的方式可以讓你在程式空閑的時候執行它們。Cocoa用NSNotificationQueue對象提供空閑時通知功能。要請求空閑時通知,就送出通知對象到預設的NSNotificationQueue(通知隊列)。隊列會延遲送出通知對象,直到run loop空閑。更多資訊請檢視Notification Programming Topics. 

待續。。。每天會持續更新。 

Asynchronous functions |系統接口包括了很多異步函數,這些函數本身就自動支援并行操作。它們可能使用系統守護程序來建立自己的線程、執行任務并傳回結果(具體的實作方式并不重要,因為它和你的代碼是分開的)。在設計程式的時候,先查查這一類支援異步操作的函數,而盡量少在自己定義的線程中用等效的同步函數。 

Timers |當遇到某些需要定時執行的小任務,而且這些任務并沒有動用線程的必要時,可以考慮在主線程中使用定時器。詳細資訊請檢視“Timer Sources.” 

Separate processes |盡管程序相對于線程來說有些“重量級”,如果某項任務和程式無直接關系,建立獨立的程序還是有必要的。例如:任務需要申請大量的記憶體空間或者必須使用root權限執行。我們還可能需要使用64位的伺服器程序來計算大資料,而用32位的程式顯示運作結果。 

4)線程的系統支援 

如果你要将線程加入到現有的代碼中去,Mac OS和iOS提供了幾種建立線程的技術。另外,這兩個系統還為管理和同步線程内的工作提供了支援。接下來的内容描述了幾種在Mac OS和iOS上使用線程的關鍵技術。 

Listing 2-2列出了在程式中可能用到的線程技術 

Table 1-2  Thread technologies 

Technology 

Description 

Cocoa threads |Cocoa使用NSThread類來實作線程。Cocoa還為NSObject類提供了一些方法來在已有線程中生成新的線程并執行代碼。更多資訊,請參考“Using NSThread” and “Using NSObject to Spawn a Thread.” 

POSIX threads |POSIX線程技術提供了一系列基于C的接口來建立線程。如果你不打算編寫Cocoa的程式,那麼它是最好的選擇。POSIX的接口相對簡單易用且擴充性良好,便于自定義線程。更多資訊,請參考“Using POSIX Threads” 

Multiprocessing Services |Multiprocessing Services是老式的C接口。被用于支援Mac OS較早的版本。這項技術在Mac OS X上可用,但在新的開發項目中應該避免使用,而應使用NSThread和POSIX的線程技術。更多資訊,請參考Multiprocessing Services Programming Guide。 

從程式的層面上來看,所有線程的行為在本質上應該是一樣的——無論是任何的平台。啟動線程後,線程會有這幾種運作狀态:正在運作(running),準備就緒(ready)和被阻塞(blocked)。如果一個線程目前未運作,那麼它可能是被阻塞,或者是在等待輸入,也有可能已經就緒,但未被安排執行(scheduled)。線程會在這幾個狀态之間來來回回。直到真正執行完畢退出,進入終結(terminated)狀态。 

當你建立一個新的線程時,你必須為其指定入口函數(在Cocoa中,叫入口方法(method))。入口函數包括你想要線上程中執行的代碼。當函數傳回,或當你顯式的終結線程時,線程就永遠結束了,然後會被系統回收。線程在記憶體和時間片占用方面代價昂貴,正因為如此,建議你在入口函數中完成大量的工作,或者自己設定一個運作回路(run loop)來循環執行工作。 

更多線程可用技術的資訊,請參考“Thread Management.” 

運作回路(Run Loops) 

運作回路是線程中管理異步事件擷取的基礎。它的工作是為線程監視事件源(event sources)。當事件到來之時,系統喚醒線程并将事件發送給運作回路,然後在傳遞給你指定的處理者。如果沒有需要處理的事件到來,運作回路将線程置于休眠狀态。 

線上程中使用運作回路不是必須的。但是如果這樣做的話,會有更好的使用者體驗。使用運作回路可以建立長期存在的線程,并盡可能少的占用資源。因為運作回路在無事可做的時候讓線程休眠。它減少了輪詢(polling)的需要,而輪詢會浪費CPU循環數,阻止CPU休眠,進而比較耗電。 

要設定一個運作回路,你所要做的隻是運作你的線程,獲得一個運作回路的對象,“安裝”你的事件處理者,并且告訴運作回路去運作。不管是Cocoa還是Carbon都在主線程中自動為你提供了預設運作回路的設定。如果你需要建立一個長期存在的子線程,就需要自己來線上程裡定義運作回路了。 

關于運作回路的詳細資訊和例子,請參考“Run Loops.”。 

同步工具 

多線程程式設計中的一個大麻煩是線程間的資源争奪。如果多個線程同時修改一份資源,問題就來了。減少問題的方法之一就是減少共享資源,保證每個線程都有自己的一份資源可以使用。當然,管理完全獨立的資源是不可能的,是以,我們要用到鎖(locks),條件控制(conditions),原子操作(atomic operations)等技術自己控制線程對資源同步的通路。 

“鎖”為代碼提供了“暴力”的保護方式,在同一時刻,代碼隻能被一個線程執行。其中,“互斥鎖”是最常用的一種形式,也被叫做互斥體(mutex)。當一個線程試圖擷取被其他線程占用的互斥體時,該線程會被阻塞,直到互斥體被釋放。幾個系統架構提供對互斥體的支援,而它們都基于同樣的底層技術。Cocoa提供額外的幾種互斥體來支援不同的操作,比如遞歸操作。更多資訊,請參考“Locks.” 

除了鎖以外,系統支援條件控制。它可以保證程式中任務按照正确的順序執行。條件控制就像一個“門衛”,它會一直阻塞某個線程。直到特定的條件為真,才允許線程繼續執行。POSIX層和Foundatoin架構都直接支援條件控制。(如果你使用操作對象(operation object),你可以設定它們之間的依賴關系,進而起到設定任務操作順序的目的,這和條件控制很類似)。 

并行程式設計中,還可以使用“原子操作”(atomic operation)來保護、同步對資料的通路。原子操作提供了一種輕量級的鎖定方案,在對某些标量資料進行數學運算或邏輯運算的時候,就可以使用原子操作。原子操作會使用特定的硬體指令來保證對于某個變量的修改完成後其他的線程才能繼續通路。 

關于同步工具的詳細資訊,請檢視“Synchronization Tools.” 

線程間通信 

雖然好的設計會盡可能減少線程間的通信,但是在某些情況下,線程間的通信是必要的(線程的任務就是為程式工作,但是如果線程的運作結果無法被使用,會有什麼影響?)線程可能需要執行新的工作請求或是向程式主線程回報工作進度。在這些情況下,你就需要把一個線程的資訊傳遞給另外一個線程。幸運的是,程式中的線程共享同一個程序空間,這意味着你有很多種通信方案。 

線程間通信有很多方式,各有優劣,“Configuring Thread-Local Storage” 表中羅列了在Mac OS X上常用到的通信機制(除了消息隊列(message queue)和Cocoa分布式對象(Cocoa distributed object)外,其他也可以在iOS上通用)。詳細清單如下: 

Table 1-3  Communication mechanisms 

Direct messaging|Cocoa程式支援直接在其他線程中執行方法(perform selector)。這個功能意味着一個線程可以直接在另外一個線程中執行方法。因為要在目标線程中執行,通過這種方法發送的消息會線上程中被自動序列化。關于input sources的詳細資訊,請檢視“Cocoa Perform Selector Sources.”(後章會有詳細說明)。 

Global variables, shared memory, and objects |另外一種通信的方式就是使用全局變量、共享對象,或是共享記憶體塊。雖然共享變量既快又簡單,這種方式相比較直接消息方式更加脆弱。必須使用鎖或者其他同步機制來“保護”共享變量。否則,可能導緻線程間的競争狀态、資料被損壞,或程式崩潰。 

Conditions|條件空之是控制線程同步的另一個工具。可以把條件控制看做是一個“門衛”,它隻會在特定條件符合的時候才允許線程執行。更多資訊,請檢視“Using Conditions.” 

Run loop sources |線上程中加入運作回路源可以讓你接收到程式的特定消息。由于運作回路是“事件驅動”的,它會在無事可做的時候會自動讓線程休眠,進而提高線程運作的效率。更多關于運作回路和運作回路源的資訊,請檢視“Run Loops.” 

Ports and sockets |基于端口的通信是一種更加複雜的方式,但是它非常穩定。更重要的是,端口和套接字可以被在外部實體的通路,比如說其他的程序(process)和服務(service)。為了提高效率,端口使用運作回路源協助執行,是以線程在端口上無等待資料的時候會自動休眠。更多關于運作回路和基于端口的輸入源(input source)方面的資訊,請檢視“Run Loops.” 

Message queues| 多程序的服務中定義了一中先進先出(FIFO)隊列的抽象模型來管理接收和傳出的資料。雖然消息隊列簡單且友善,但它不像其他通信技術那樣高效。關于如何使用消息隊列,請檢視Multiprocessing Services Programming Guide. 

Cocoa distributed objects|分布式對象是Cocoa技術中的一項,它為基于端口通信的實作提供了一種進階的方式。雖然也可以把分布式對象用線上程間通信中,但是不建議這樣做,因為會導緻很高的系統開銷。分布式對象在程序間通信中可以很好的排上用場,因為程序間通信本身就已經有較高的開銷了。 

繼續閱讀