iOS多線程編程指南(一)關於多線程編程(轉)

原文:http://www.dreamingwish.com/article/ios-multi-threaded-programming-a-multi-threaded-programming.html

第一章      關於多線程編程

多年來,計算機的最大性能主要受限於它的中心微處理器的速度。然而因爲個別處理器已經開始達到它的瓶頸限制,芯片製造商開始轉向多核設計,讓計算機具備了同時執行多個任務的能力。儘管Mac OS X利用了這些核心優點,在任什麼時候候能夠執行系統相關的任務,但本身的應用程序也能夠經過多線程方法利用這些優點。html

1.1        什麼是多線程

多線程是一個比較輕量級的方法來實現單個應用程序內多個代碼執行路徑。在系統級別內,程序並排執行,系統分配到每一個程序的執行時間是基於該程序的所需時間和其餘程序的所需時間來決定的。然而在每一個應程序的內部,存在一個或多個執行線程,它同時或在一個幾乎同時發生的方式裏執行不一樣的任務。系統自己管理這些執行的線程,調度它們在可用的內核上運行,並在須要讓其餘線程執行的時候搶先打斷它們。ios

從技術角度來看,一個線程就是一個須要管理執行代碼的內核級和應用級數據結構組合。內核級結構協助調度線程事件,並搶佔式調度一個線程到可用的內核之上。應用級結構包括用於存儲函數調用的調用堆棧和應用程序須要管理和操做線程屬性和狀態的結構。編程

在非併發的應用程序,只有一個執行線程。該線程開始和結束於你應用程序的main循環,一個個方法和函數的分支構成了你整個應用程序的全部行爲。與此相反,支持併發的應用程序開始能夠在須要額外的執行路徑時候建立一個或多個線程。每一個新的執行路徑有它本身獨立於應用程序main循環的定製開始循環。在應用程序中存在多個線程提供了兩個很是重要的的潛在優點:安全

  1. 多個線程能夠提升應用程序的感知響應。
  2. 多個線程能夠提升應用程序在多核系統上的實時性能。

若是你的應用程序只有單獨的線程,那麼該獨立程序須要完成全部的事情。它必須對事件做出響應,更新您的應用程序的窗口,並執行全部實現你應用程序行爲須要的計算。擁有單獨線程的主要問題是在同一時間裏面它只能執行一個任務。那麼當你的應用程序須要很長時間才能完成的時候會發生什麼呢?當你的代碼忙於計算你所須要的值的時候,你的程序就會中止響應用戶事件和更新它的窗口。若是這樣的狀況持續足夠長的時間,用戶就會誤認爲你的程序被掛起了,並試圖強制退出。若是你把你的計算任務轉移到一個獨立的線程裏面,那麼你的應用程序主線程就能夠自由並及時響應用戶的交互。數據結構

固然多線程並非解決程序性能問題的靈丹妙藥。多線程帶來好處同時也伴隨着潛在問題。應用程序內擁有多個可執行路徑,會給你的代碼增長更多的複雜性。每一個線程須要和其餘線程協調其行爲,以防止它破壞應用程序的狀態信息。由於應用程序內的多個線程共享內存空間,它們訪問相同的數據結構。若是兩個線程試圖同時處理相同的數據結構,一個線程有可能覆蓋另外線程的改動致使破壞該數據結構。即便有適當的保護,你仍然要注意因爲編譯器的優化致使給你代碼產生很微妙的(和不那麼微妙)的Bug。多線程

1.2        線程術語

在討論多線程和它支持的相關技術以前,咱們有必要先了解一些基本的術語。若是你熟悉Carbon的多處理器服務API或者UNIX系統的話,你會發現本文檔裏面「任務(task)」被用於不一樣的定義。在Mac OS的早期版本,術語「任務(task)」是用來區分使用多處理器服務建立的線程和使用Carbon線程管理API建立的線程。在UNIX系統裏面,術語「任務(task)」也在一段時間內被用於指代運行的進程。在實際應用中,多處理器服務任務是至關於搶佔式的線程。併發

因爲Carbon線程管理器和多處理器服務API是Mac OS X的傳統技術,本文件採用下列術語:app

  1. 線程(線程)用於指代獨立執行的代碼段。
  2. 進程(process)用於指代一個正在運行的可執行程序,它能夠包含多個線程。
  3. 任務(task)用於指代抽象的概念,表示須要執行工做。

 

 

1.3        多線程的替代方法

你本身建立多線程代碼的一個問題就是它會給你的代碼帶來不肯定性。多線程是一個相對較低的水平和複雜的方式來支持你的應用程序併發。若是你不徹底理解你的設計選擇的影響,你可能很容易遇到同步或定時問題,其範圍能夠從細微的行爲變化到嚴重到讓你的應用程序崩潰並破壞用戶數據。框架

你須要考慮的另外一個因素是你是否真的須要多線程或併發。多線程解決了如何在同一個進程內併發的執行多路代碼路徑的問題。然而在不少狀況下你是沒法保證你所在作的工做是併發的。多線程引入帶來大量的開銷,包括內存消耗和CPU佔用。你會發現這些開銷對於你的工做而言實在太大,或者有其餘方法會更容易實現。異步

表1-1列舉了多線程的替代方法。該表包含了多線程的替代技術(好比操做對象和GCD)和如何更高效的使用單個線程。

Table 1-1  Alternative technologies to threads

Technology

Description

Operation objects

Introduced in Mac OS X v10.5, an operation object is a wrapper for a task that would normally be executed on a secondary thread. This wrapper hides the thread management aspects of performing the task, leaving you free to focus on the task itself. You typically use these objects in conjunction with an operation queue object, which actually manages the execution of the operation objects on one more threads.
For more information on how to use operation objects, see Concurrency Programming Guide.

Grand Central Dispatch (GCD)

Introduced in Mac OS x v10.6, Grand Central Dispatch is another alternative to threads that lets you focus on the tasks you need to perform rather than on thread management. With GCD, you define the task you want to perform and add it to a work queue, which handles the scheduling of your task on an appropriate thread. Work queues take into account the number of available cores and the current load to execute your tasks more efficiently than you could do yourself using threads.
For information on how to use GCD and work queues, see Concurrency Programming Guide

Idle-time notifications

For tasks that are relatively short and very low priority, idle time notifications let you perform the task at a time when your application is not as busy. Cocoa provides support for idle-time notifications using theNSNotificationQueue object. To request an idle-time notification, post a notification to the default NSNotificationQueue object using the NSPostWhenIdle option. The queue delays the delivery of your notification object until the run loop becomes idle. For more information, see Notification Programming Topics.

Asynchronous functions

The system interfaces include many asynchronous functions that provide automatic concurrency for you. These APIs may use system daemons and processes or create custom threads to perform their task and return the results to you. (The actual implementation is irrelevant because it is separated from your code.) As you design your application, look for functions that offer asynchronous behavior and consider using them instead of using the equivalent synchronous function on a custom thread.

Timers

You can use timers on your application’s main thread to perform periodic tasks that are too trivial to require a thread, but which still require servicing at regular intervals. For information on timers, see 「Timer Sources.」

Separate processes

Although more heavyweight than threads, creating a separate process might be useful in cases where the task is only tangentially related to your application. You might use a process if a task requires a significant amount of memory or must be executed using root privileges. For example, you might use a 64-bit server process to compute a large data set while your 32-bit application displays the results to the user.

注意:當使用fork函數加載獨立進程的時候,你必須老是在fork後面調用exec或者相似的函數。基於Core Foundation、Cocao或者Core Data框架(不管顯式仍是隱式關聯)的應用程序隨後調用exec函數或者相似的函數都會導出不肯定的結果。

1.4        線程支持

若是你已經有代碼使用了多線程,Mac OS X和iOS提供幾種技術來在你的應用程序裏面建立多線程。此外,兩個系統都提供了管理和同步你須要在這些線程裏面處理的工做。如下幾個部分描述了一些你在Mac OS X和iOS上面使用多線程的時候須要注意的關鍵技術。

1.4.1    線程包

雖然多線程的底層實現機制是Mach的線程,你不多(即便有)使用Mach級的線程。相反,你會常用到更多易用的POSIX 的API或者它的衍生工具。Mach的實現沒有提供多線程的基本特徵,可是包括搶佔式的執行模型和調度線程的能力,因此它們是相互獨立的。

列表1-2列舉你能夠在你的應用程序使用的線程技術。

Table 1-2  Thread technologies

Technology

Description

Cocoa threads

Cocoa implements threads using the NSThread class. Cocoa also provides methods on NSObject for spawning new threads and executing code on already-running threads. For more information, see 「Using NSThread」 and 「Using NSObject to Spawn a Thread.」

POSIX threads

POSIX threads provide a C-based interface for creating threads. If you are not writing a Cocoa application, this is the best choice for creating threads. The POSIX interface is relatively simple to use and offers ample flexibility for configuring your threads. For more information, see 「Using POSIX Threads」

Multiprocessing Services

Multiprocessing Services is a legacy C-based interface used by applications transitioning from older versions of Mac OS. This technology is available in Mac OS X only and should be avoided for any new development. Instead, you should use the NSThread class or POSIX threads. If you need more information on this technology, seeMultiprocessing Services Programming Guide.

    在應用層上,其餘平臺同樣全部線程的行爲本質上是相同的。線程啓動以後,線程就進入三個狀態中的任何一個:運行(running)、就緒(ready)、阻塞(blocked)。若是一個線程當前沒有運行,那麼它不是處於阻塞,就是等待外部輸入,或者已經準備就緒等待分配CPU。線程持續在這三個狀態之間切換,直到它最終退出或者進入中斷狀態。

當你建立一個新的線程,你必須指定該線程的入口點函數(或Cocoa線程時候爲入口點方法)。該入口點函數由你想要在該線程上面執行的代碼組成。但函數返回的時候,或你顯式的中斷線程的時候,線程永久中止,且被系統回收。由於線程建立須要的內存和時間消耗都比較大,所以建議你的入口點函數作至關數量的工做,或創建一個運行循環容許進行常常性的工做。

爲了獲取更多關於線程支持的可用技術而且如何使用它們,請閱讀「線程管理部分」。

1.4.2    Run Loops

注:爲了便於記憶,文本後面部分翻譯Run Loops的時候基本採用原義,而非翻譯爲「運行循環」。

    一個run loop是用來在線程上管理事件異步到達的基礎設施。一個run loop爲線程監測一個或多個事件源。當事件到達的時候,系統喚醒線程並調度事件到run loop,而後分配給指定程序。若是沒有事件出現和準備處理,run loop把線程置於休眠狀態。

你建立線程的時候不須要使用一個run loop,可是若是你這麼作的話能夠給用戶帶來更好的體驗。Run Loops可讓你使用最小的資源來建立長時間運行線程。由於run loop在沒有任何事件處理的時候會把它的線程置於休眠狀態,它消除了消耗CPU週期輪詢,並防止處理器自己進入休眠狀態並節省電源。

爲了配置run loop,你所須要作的是啓動你的線程,獲取run loop的對象引用,設置你的事件處理程序,並告訴run loop運行。Cocoa和Carbon提供的基礎設施會自動爲你的主線程配置相應的run loop。若是你打算建立長時間運行的輔助線程,那麼你必須爲你的線程配置相應的run loop。

關於run loops的詳細信息和如何使用它們的例子會在「Run Loops」部分介紹。

1.4.3    同步工具

線程編程的危害之一是在多個線程之間的資源爭奪。若是多個線程在同一個時間試圖使用或者修改同一個資源,就會出現問題。緩解該問題的方法之一是消除共享資源,並確保每一個線程都有在它操做的資源上面的獨特設置。由於保持徹底獨立的資源是不可行的,因此你可能必須使用鎖,條件,原子操做和其餘技術來同步資源的訪問。

鎖提供了一次只有一個線程能夠執行代碼的有效保護形式。最廣泛的一種鎖是互斥排他鎖,也就是咱們一般所說的「mutex」。當一個線程試圖獲取一個當前已經被其餘線程佔據的互斥鎖的時候,它就會被阻塞直到其餘線程釋放該互斥鎖。系統的幾個框架提供了對互斥鎖的支持,雖然它們都是基於相同的底層技術。此外Cocoa提供了幾個互斥鎖的變種來支持不一樣的行爲類型,好比遞歸。獲取更多關於鎖的種類的信息,請閱讀「鎖」部份內容。

除了鎖,系統還提供了條件,確保在你的應用程序任務執行的適當順序。一個條件做爲一個看門人,阻塞給定的線程,直到它表明的條件變爲真。當發生這種狀況的時候,條件釋放該線程並容許它繼續執行。POSIX級別和基礎框架都直接提供了條件的支持。(若是你使用操做對象,你能夠配置你的操做對象之間的依賴關係的順序肯定任務的執行順序,這和條件提供的行爲很是類似)。

儘管鎖和條件在併發設計中使用很是廣泛,原子操做也是另一種保護和同步訪問數據的方法。原子操做在如下狀況的時候提供了替代鎖的輕量級的方法,其中你能夠執行標量數據類型的數學或邏輯運算。原子操做使用特殊的硬件設施來保證變量的改變在其餘線程能夠訪問以前完成。

獲取更多關於可用同步工具信息,請閱讀「同步工具」部分。

1.4.4    線程間通訊

雖然一個良好的設計最大限度地減小所需的通訊量,但在某些時候,線程之間的通訊顯得十分必要。(線程的任務是爲你的應用程序工做,但若是歷來沒有使用過這些工做的結果,那有什麼好處呢?)線程可能須要處理新的工做要求,或向你應用程序的主線程報告其進度狀況。在這些狀況下,你須要一個方式來從其餘線程獲取信息。幸運的是,線程共享相同的進程空間,意味着你能夠有大量的可選項來進行通訊。

線程間通訊有不少種方法,每種都有它的優勢和缺點。「配置線程局部存儲」列出了不少你能夠在Mac OS X上面使用的通訊機制。(異常的消息隊列和Cocoa分佈式對象,這些技術也可在iOS用來通訊)。本表中的技術是按照複雜性的順序列出。

Table 1-3  Communication mechanisms

Mechanism

Description

Direct messaging

Cocoa applications support the ability to perform selectors directly on other threads. This capability means that one thread can essentially execute a method on any other thread. Because they are executed in the context of the target thread, messages sent this way are automatically serialized on that thread. For information about input sources, see 「Cocoa Perform Selector Sources.」

Global variables, shared memory, and objects

Another simple way to communicate information between two threads is to use a global variable, shared object, or shared block of memory. Although shared variables are fast and simple, they are also more fragile than direct messaging. Shared variables must be carefully protected with locks or other synchronization mechanisms to ensure the correctness of your code. Failure to do so could lead to race conditions, corrupted data, or crashes.

Conditions

Conditions are a synchronization tool that you can use to control when a thread executes a particular portion of code. You can think of conditions as gate keepers, letting a thread run only when the stated condition is met. For information on how to use conditions, see 「Using Conditions.」

Run loop sources

A custom run loop source is one that you set up to receive application-specific messages on a thread. Because they are event driven, run loop sources put your thread to sleep automatically when there is nothing to do, which improves your thread’s efficiency. For information about run loops and run loop sources, see 「Run Loops.」

Ports and sockets

Port-based communication is a more elaborate way to communication between two threads, but it is also a very reliable technique. More importantly, ports and sockets can be used to communicate with external entities, such as other processes and services. For efficiency, ports are implemented using run loop sources, so your thread sleeps when there is no data waiting on the port. For information about run loops and about port-based input sources, see「Run Loops.」

Message queues

The legacy Multiprocessing Services defines a first-in, first-out (FIFO) queue abstraction for managing incoming and outgoing data. Although message queues are simple and convenient, they are not as efficient as some other communications techniques. For more information about how to use message queues, see Multiprocessing Services Programming Guide.

Cocoa distributed objects

Distributed objects is a Cocoa technology that provides a high-level implementation of port-based communications. Although it is possible to use this technology for inter-thread communication, doing so is highly discouraged because of the amount of overhead it incurs. Distributed objects is much more suitable for communicating with other processes, where the overhead of going between processes is already high. For more information, seeDistributed Objects Programming Topics.

1.1        設計技巧

如下各節幫助你實現本身的線程提供了指導,以確保你代碼的正確性。部分指南同時提供如何利用你的線程代碼得到更好的性能。任何性能的技巧,你應該在你更改你代碼以前、期間、以後老是收集相關的性能統計數據。

1.1.1    避免顯式建立線程

手動編寫線程建立代碼是乏味的,並且容易出現錯誤,你應該儘量避免這樣作。Mac OS X和iOS經過其餘API接口提供了隱式的併發支持。你能夠考慮使用異步API,GCD方式,或操做對象來實現併發,而不是本身建立一個線程。這些技術背後爲你作了線程相關的工做,並保證是無誤的。此外,好比GCD和操做對象技術被設計用來管理線程,比經過本身的代碼根據當前的負載調整活動線程的數量更高效。 關於更多GCD和操做對象的信息,你能夠查閱「併發編程指南(Concurrency Programming Guid)」。

1.1.2    保持你的線程合理的忙

若是你準備人工建立和管理線程,記得多線程消耗系統寶貴的資源。你應該盡最大努力確保任何你分配到線程的任務是運行至關長時間和富有成效的。同時你不該該懼怕中斷那些消耗最大空閒時間的線程。線程使用一個平凡的內存量,它的一些有線,因此釋放一個空閒線程,不只有助於下降您的應用程序的內存佔用,它也釋放出更多的物理內存使用的其餘系統進程。線程佔用必定量的內存,其中一些是有線的,因此釋放空閒線程不但幫助你減小了你應用程序的內存印記,並且還能釋放出更多的物理內存給其餘系統進程使用。

重要:在你中斷你的空閒線程開始以前,你必須老是記錄你應用程序當前的性能基線測量。當你嘗試修改後,採起額外的測量來確保你的修改實際上提升了性能,而不是對它操做損害。

1.1.3    避免共享數據結構

避免形成線程相關資源衝突的最簡單最容易的辦法是給你應用程序的每一個線程一份它需求的數據的副本。當最小化線程之間的通訊和資源爭奪時並行代碼的效果最好。

建立多線程的應用是很困難的。即便你很是當心,而且在你的代碼裏面全部正確的地方鎖住共享資源,你的代碼依然可能語義不安全的。好比,當在一個特定的順序裏面修改共享數據結構的時候,你的代碼有可能遇到問題。以原子方式修改你的代碼,來彌補可能隨後對多線程性能產生損耗的狀況。把避免資源爭奪放在首位一般能夠獲得簡單的設計一樣具備高性能的效果。

1.1.4    多線程和你的用戶界面

若是你的應用程序具備一個圖形用戶界面,建議你在主線程裏面接收和界面相關的事件和初始化更新你的界面。這種方法有助於避免與處理用戶事件和窗口繪圖相關的同步問題。一些框架,好比Cocoa,一般須要這樣操做,可是它的事件處理能夠不這樣作,在主線程上保持這種行爲的優點在於簡化了管理你應用程序用戶界面的邏輯。

有幾個顯著的例外,它有利於在其餘線程執行圖形操做。好比,QuickTime API包含了一系列能夠在輔助線程執行的操做,包括打開視頻文件,渲染視頻文件,壓縮視頻文件,和導入導出圖像。相似的,在Carbon和Cocoa裏面,你可使用輔助線程來建立和處理圖片和其餘圖片相關的計算。使用輔助線程來執行這些操做能夠極大提升性能。若是你不肯定一個操做是否和圖像處理相關,那麼你應該在主線程執行這些操做。

關於QuickTime線程安全的信息,查閱Technical Note TN2125:「QuickTime的線程安全編程」。關於Cocoa線程安全的更多信息,查閱「線程安全總結」。關於Cocoa繪畫信息,查閱Cocoa繪畫指南(Cocoa Drawing Guide)。

1.1.5    瞭解線程退出時的行爲

進程一直運行直到全部非獨立線程都已經退出爲止。默認狀況下,只有應用程序的主線程是以非獨立的方式建立的,可是你也可使用一樣的方法來建立其餘線程。當用戶退出程序的時候,一般考慮適當的當即中斷全部獨立線程,由於一般獨立線程所作的工做都是是可選的。若是你的應用程序使用後臺線程來保存數據到硬盤或者作其餘週期行的工做,那麼你可能想把這些線程建立爲非獨立的來保證程序退出的時候不丟失數據。

以非獨立的方式建立線程(又稱做爲可鏈接的)你須要作一些額外的工做。由於大部分上層線程封裝技術默認狀況下並無提供建立可鏈接的線程,你必須使用POSIX API來建立你想要的線程。此外,你必須在你的主線程添加代碼,來當它們最終退出的時候鏈接非獨立的線程。更多有關建立可鏈接的線程信息,請查閱「設置線程的脫離狀態」部分。

若是你正在編程Cocoa的程序,你也能夠經過使用applicationShouldTerminate:的委託方法來延遲程序的中斷直到一段時間後或者完成取消。當延遲中斷的時候,你的程序須要等待直到任何週期線程已經完成它們的任務且調用了replyToApplicationShouldTerminate:方法。關於更多這些方法的信息,請查閱NSApplication Class Reference。

1.1.6    處理異常

當拋出一個異常時,異常的處理機制依賴於當前調用堆棧執行任何須要的清理。由於每一個線程都有它本身的調用堆棧,因此每一個線程都負責捕獲它本身的異常。若是在輔助線程裏面捕獲一個拋出的異常失敗,那麼你的主線程也一樣捕獲該異常失敗:它所屬的進程就會中斷。你沒法捕獲同一個進程裏面其餘線程拋出的異常。

若是你須要通知另外一個線程(好比主線程)當前線程中的一個特殊狀況,你應該捕捉異常,並簡單地將消息發送到其餘線程告知發生了什麼事。根據你的模型和你正在嘗試作的事情,引起異常的線程能夠繼續執行(若是可能的話),等待指示,或者乾脆退出。

注意:在Cocoa裏面,一個NSException對象是一個自包含對象,一旦它被引起了,那麼它能夠從一個線程傳遞到另一個線程。

在一些狀況下,異常處理多是自動建立的。好比,Objective-C中的@synchronized包含了一個隱式的異常處理。

1.1.7    乾淨地中斷你的線程

線程天然退出的最好方式是讓它達到其主入口結束點。雖然有很多函數能夠用來當即中斷線程,可是這些函數應僅用於做爲最後的手段。在線程達到它天然結束點以前中斷一個線程阻礙該線程清理完成它本身。若是線程已經分配了內存,打開了文件,或者獲取了其餘類型資源,你的代碼可能沒辦法回收這些資源,結果形成內存泄漏或者其餘潛在的問題。

關於更多正確退出線程的信息,請查閱「中斷線程」部分。

1.1.8    線程安全的庫

雖然應用程序開發人員控制應用程序是否執行多個線程,類庫的開發者則沒法這樣控制。當開發類庫時,你必須假設調用應用程序是多線程,或者多線程之間能夠隨時切換。所以你應該老是在你的臨界區使用鎖功能。

對類庫開發者而言,只當應用程序是多線程的時候才建立鎖是不明智的。若是你須要鎖定你代碼中的某些部分,早期應該建立鎖對象給你的類庫使用,更好是顯式調用初始化類庫。雖然你也可使用靜態庫的初始化函數來建立這些鎖,可是僅當沒有其餘方式的才應該這樣作。執行初始化函數須要延長加載你類庫的時間,且可能對你程序性能形成不利影響。

注意:永遠記住在你的類庫裏面保持鎖和釋放鎖的操做平衡。你應該老是記住鎖定類庫的數據結構,而不是依賴調用的代碼提供線程安全環境。

若是你真正開發Cocoa的類庫,那麼當你想在應用程序變成多線程的時候收到通知的話,你能夠給NSWillBecomeMultiThreadedNotification 註冊一個觀察者。不過你不該用依賴於這些收到的通知,由於它們可能在你的類庫被調用以前已經被髮出了。

相關文章
相關標籤/搜索