由于文章長度限制,本文作為[譯]線程編程指南(一)后續(xù)部分。
Run Loops
Run loop是與線程相關的基礎結構之一。Run loop是一個用來調度工作并接收事件的事件處理循環(huán)。Run loop其目的在于有任務的時候讓線程保持忙碌狀態(tài)而無事可做時讓線程睡眠。
Run loop管理并不是完全自動的。你還必須設計出線程代碼在適當?shù)臅r間啟動run loop,并響應傳入的事件。Cocoa和Core Foundation提供run loop對象以幫助你配置和管理線程的run loop。應用程序不需要顯式地創(chuàng)建這些對象;每一個線程,包括應用程序的主線程,都有一個相關的run loop對象。只有輔助線程才需要顯式地運行它們的run loop。應用程序會自動設置和運行主線程的run loop并將其作為程序啟動過程的一部分。
以下各節(jié)提供有關run loop的更多信息,以及如何在應用程序中配置它們。
剖析Run Loop
Run loop正如其名稱一樣,它是一個讓線程進入并用來運行事件回調的循環(huán)。你的代碼提供的控制語句會用于實現(xiàn)run loop中的實際循環(huán)部分,換句話說,你的代碼中的while和for循環(huán)驅動著run loop的運行。在run loop內,你將使用run loop對象來驅動事件處理代碼執(zhí)行以接收事件和調用處理回調。
Run loop通過兩種源來接收事件。輸入源傳遞異步的事件,通常是其他線程和另一個應用傳入。定時源傳遞同步事件,發(fā)生在調度時期或者重復周期。**當相關事件到達時兩種類型的源都會使用應用指定的回調例程來處理事件。
圖3-1展示了run loop及其不同類型源的概念結構。輸入源傳遞異步事件給相應的回調并引起runUntilDate:方法調用(實際由線程的NSRunLoop對象調用)以退出run loop。定時源傳遞事件給回調例程但并不導致run loop退出。
圖3-1 Run loop及其源結構
[圖片上傳失敗...(image-725270-1536734662071)]
除了處理輸入源之外,run loop同樣會為其行為創(chuàng)建通知。注冊一個run loop的觀察者可以獲取到這些通知并使用它們在線程中做進一步的操作。
以下章節(jié)將提供有關run loop構成和該模式如何操作的更多細節(jié)。同樣會描述事件處理的不同階段通知的創(chuàng)建過程。
Run Loop模式
一個run loop的模式包括了一系列待監(jiān)測的輸入源、定時源以及待通知的觀察者。每次運行run loop時,都會(顯式或隱式地)為其指定一個運行“模式”。在run loop過程中,只監(jiān)測與該模式相關聯(lián)的源,并允許相關的事件傳遞。(類似地,只有與該模式相關聯(lián)的觀察者通知run loop執(zhí)行)與其他模式相關聯(lián)來源于任何新事件的消息,直到隨后在適當模式下才進入循環(huán)。
在代碼當中,使用名稱來定義模式。Cocoa和Core Foundation定義了默認模式和幾個常用模式,在代碼中都是以字符串的形式表示。你也可以定義一個自定義模式并用字符串為模式命名。雖然自定義模式的名稱是任意的,但模式的內容卻不是。你必須確認為其加入一個或多個輸入源、定時源、觀察者才會使其產生真正的效果。
你可以使用模式來過濾掉run loop中不感興趣的事件。多數(shù)情況下,你會希望run loop運行在系統(tǒng)默認定義的“default”模式下。對于“模態(tài)面板”,可能會運行在“模態(tài)”模式下。當進入該模式時,只有源相關的模態(tài)面板才能在線程中傳遞事件。對于輔助線程而言,你需要使用自定義模式來防止低優(yōu)先級源在時間敏感的操作中傳遞事件。
注意:模式基于事件的源區(qū)分而不是事件類型。例如,你不能使用模式來僅匹配鼠標點擊事件或者鍵盤事件。你應該使用模式來監(jiān)聽不同的端口,偶爾掛起定時器,或者切換當前被監(jiān)測源和觀察者。
表3-1列舉了Cocoa和Core Foundation中的標準模式及其如何使用的描述。名稱字段列舉了在代碼中使用該模式的名稱常量。
表3-1 預定義的run loop模式
| 模式 | 名稱 | 描述 |
|---|---|---|
| 默認(Default) | NSDefaultRunLoopMode (Cocoa)、kCFRunLoopDefaultMode (Core Foundation) | 大多數(shù)操作使用的默認模式。大多數(shù)時間,你會使用該模式啟動run loop并配置輸入源。 |
| 連接(Connection) | NSConnectionReplyMode (Cocoa) | Cocoa使用該模式來連接NSConnection對象以監(jiān)視其回應。你自身很少會用到這種模式。 |
| 模態(tài)(Modal) | NSModalPanelRunLoopMode (Cocoa) | Cocoa使用該模式來追蹤相關模態(tài)面板的事件。 |
| 事件追蹤(Event tracking) | NSEventTrackingRunLoopMode (Cocoa) | Cocoa使用該模式來限制其他事件進入當鼠標拖動期間以及其他用戶界面跟蹤期間。 |
| 通用模式(Common modes) | NSRunLoopCommonModes (Cocoa)、kCFRunLoopCommonModes (Core Foundation) | 這是一個可配置的通用模式組。為其中的一個模式關聯(lián)輸入源的同時會為該組中的所有模式關聯(lián)該輸入源。對于Cocoa應用,這個集合默認包含了上述全部模式。對于Core Foundation來講則最初只包含了默認模式。你可以使用CFRunLoopAddCommonMode函數(shù)向其添加自定義模式。 |
輸入源
輸入源在你的線程中異步地傳遞事件。事件的來源取決于輸入源的類型,通常是兩種中的一個類型。基于端口的輸入源在你應用的Mach端口上進行監(jiān)聽。自定義的輸入源監(jiān)聽自定義的事件源。只要是涉及到run loop,一個輸入源是基于端口的或是自定義的并不重要。系統(tǒng)通常會實現(xiàn)兩種類型以供使用。這兩種源唯一區(qū)別在于它們通知方式不同?;诙丝诘脑从蓛群俗詣油ㄖ?,自定義的源需要從其他線程手動通知。
當你創(chuàng)建完輸入源之后,可以將其指定到run loop的一個或多個模式下。在任何時刻模式都會影響到輸入源的受監(jiān)視情況。在大多數(shù)時間,你會在默認模式下運行run loop,但是你仍可以指定到自定義模式下運行。如果輸入源不在當前監(jiān)視的模式下,任何它產生的事件會被保留直到下一次run loop運行到正確的模式。
以下具體描述了各種類型的輸入源。
基于端口的輸入源
Cocoa和Core Foundation提供了使用基于端口的對象和函數(shù)來創(chuàng)建基于端口輸入源的內建支持。例如,在Cocoa中,你不必直接創(chuàng)建輸入源。你僅需使用NSPort的方法創(chuàng)建端口對象并將該端口添加到run loop。端口對象會為你處理創(chuàng)建和配置所需的輸入源。
在Core Foundation中,你必須同時手動創(chuàng)建端口和run loop源。在這種情況下,你可以使用端口相關的不透明類型(CFMachPortRef,CFMessagePortRef,或CFSocketRef)的函數(shù)來創(chuàng)建合適的對象。
自定義輸入源
為創(chuàng)建自定義輸入源,你必須使用Core Foundation中CFRunLoopSourceRef類型相關的函數(shù),并使用多個不同的回調函數(shù)來配置自定義的輸入源。Core Foundation調用這些函數(shù)來配置源,處理任何到達事件,以及當源從run loop移除時銷毀它們。
當事件到達時,定義自定義源的行為的同時,你必須定義事件的傳遞機制。源的這部分運行在一個單獨的線程中,負責為輸入源提供數(shù)據(jù),并在數(shù)據(jù)準備好時發(fā)出信號。事件傳遞機制是取決于需求而不必過于復雜。
Cocoa執(zhí)行選擇器源
除了基于端口的輸入源之外,Cocoa定義了一種可以允許在任何線程上執(zhí)行選擇器的自定義輸入源。同基于端口的輸入源類似,執(zhí)行選擇器的請求被序列化到目標線程,緩解了同一線程上運行的多個方法間可能存在的同步問題的產生。與基于端口的源不同的是,執(zhí)行選擇器的源會在執(zhí)行完選擇器之后將自身從run loop上移除。
注意:早在OS X 10.5之前,執(zhí)行選擇器源被主要用于主線程的消息發(fā)送上,但在OS X 10.5之后以及iOS上,你可以使用它們向任何線程發(fā)送消息。
當在另一個線程上執(zhí)行選擇器時,目標線程必須有一個活動的run loop。這意味著線程一旦創(chuàng)建,你的代碼就必須顯式地開啟一個run loop。由于主線程會開啟它自身的run loop,然而,你也可以在應用的委托回調方法applicationDidFinishLaunching:完成該調用。Run loop會在每次通過循環(huán)時完成所有隊列中的選擇器調用,而不是在每次循環(huán)中只處理一個。
表3-2列舉了NSObject中可用于在其他線程上執(zhí)行選擇器的方法。由于這些方法都由NSObject聲明,所以你能夠在任何能訪問到Objective-C對象的線程上使用它們,包括POSIX線程。這些方法事實上不會創(chuàng)建新的線程來執(zhí)行選擇器。
表3-2 在其他線程上執(zhí)行選擇器
| 名稱 | 描述 |
|---|---|
performSelectorOnMainThread:withObject:waitUntilDone:performSelectorOnMainThread:withObject:waitUntilDone:modes:
|
在應用主線程的下一個run loop周期內執(zhí)行指定的選擇器。該方法會阻塞當前線程直到選擇器執(zhí)行完畢。 |
performSelector:onThread:withObject:waitUntilDone:performSelector:onThread:withObject:waitUntilDone:modes:
|
在任何一個NSThread對象的線程上執(zhí)行指定選擇器。該方法會阻塞當前線程直到選擇器執(zhí)行完畢。 |
performSelector:withObject:afterDelay:performSelector:withObject:afterDelay:inModes:
|
在當前線程的下一個run loop周期內的選擇性延遲時間后執(zhí)行指定的選擇器。由于它會等到下一個run loop的周期才執(zhí)行,所以該方法提供了一個從當前起自動的最小延遲執(zhí)行時間。隊列中的多個選擇器在排列好后會依次執(zhí)行。 |
cancelPreviousPerformRequestsWithTarget:cancelPreviousPerformRequestsWithTarget:selector:object:
|
取消上一個執(zhí)行請求。 |
定時源
定時源在將來的一個預定時間向線程同步地傳遞事件。定時器作為線程通知自身完成某些工作的一種方式。例如,搜索框一旦在用戶連續(xù)輸入關鍵字間隙可以使用計時器來發(fā)起自動搜索。在開始搜索之前,搜索操作的延時可以讓用戶盡可能多的輸入所需的關鍵字符串。
盡管定時源創(chuàng)建了基于時間的通知,但定時器并不是一個實時機制。類似輸入源,定時器與run loop的具體模式關聯(lián)。如果定時器在run loop模式中當前沒有處于被監(jiān)控狀態(tài),它會直到run loop運行到支持的模式下才開啟。同理,如果定時器在run loop執(zhí)行到回調中是觸發(fā),該定時器會等到下一次進入run loop時才回去調用回調。如果run loop根本就沒有運行,定時器永遠不會被觸發(fā)。
你可以創(chuàng)建定時器以一次或者重復多次地產生事件。重復性的定時器會自動基于其調度觸發(fā)時間重新調度,而并不是實際的觸發(fā)時間。例如,如果一個定時器原定于每5秒完成一次觸發(fā),調度觸發(fā)時間總是落在原有的5秒間隔上,即便實際的觸發(fā)時間產生了延遲。如果觸發(fā)時間被延遲過多則會導致其錯過一次或多次的調度觸發(fā)機會,使得定時器在某一段時間內只觸發(fā)了一次。在調度丟失的時間完成觸發(fā)之后,定時器會在下一次調度觸發(fā)時間重新調度。
Run Loop觀察者
對比那些在合適時間觸發(fā)或同步或異步的事件的源,run loop觀察者則在run loop自身執(zhí)行的特定位置觸發(fā)事件。你可以使用run loop觀察者對線程的既定事件做準備或者在線程睡眠之前做好準備。你可以在run loop中為觀察者關聯(lián)如下事件:
- Run loop進入。
- Run loop將要處理定時器。
- Run loop將要處理輸入源。
- Run loop將要睡眠。
- Run loop將要喚醒,但在其處理事件之前。
- Run loop退出。
你可以使用Core Foundation提供的方式來為應用創(chuàng)建run loop觀察者。為創(chuàng)建run loop觀察者,你需要創(chuàng)建CFRunLoopObserverRef類型的實例。該類型會追蹤自定義的回調函數(shù)以及感興趣的活動。
和定時器一樣,觀察者可以被一次性或重復性的使用。一次性觀察者在觸發(fā)之后會將自己從run loop中移除,而重復執(zhí)行的觀察者則繼續(xù)保留。你可以在觀察者創(chuàng)建時指定其是一次性的還是重復性的。
Run Loop事件隊列
在每次run loop運行時,線程的run loop執(zhí)行掛起事件并為任何關聯(lián)的觀察者創(chuàng)建通知。這些事件的通知順序如下:
- 通知觀察者run loop進入。
- 通知觀察者就緒的定時器即將觸發(fā)。
- 通知觀察者non-port-based(非基于端口)輸入源即將觸發(fā)。
- 觸發(fā)已就緒的non-port-based輸入源。
- 如果基于端口的輸入源等到觸發(fā),立即處理該事件。跳到步驟9。
- 通知觀察者線程即將睡眠。
- 保持線程睡眠直到下列其中一個事件發(fā)生:
- 基于端口的輸入源事件到達。
- 定時器觸發(fā)。
- Run loop超時。
- Run loop被顯式地喚醒。
- 通知觀察者線程喚醒。
- 處理掛起事件。
- 如果用戶定義的定時器觸發(fā),處理定時器事件并重啟run loop。跳到步驟2。
- 如果輸入源觸發(fā),傳遞該事件。
- 如果run loop顯式地喚醒且并不超時,重啟run loop。跳到步驟2。
- 通知觀察者run loop已退出。
由于觀察者對定時器和輸入源的通知是在相應事件實際發(fā)生之前傳遞的,所以通知的時間和事件實際發(fā)生的時間可能存在間隔。如果這些事件之間的時間至關重要,你可以使用sleep和awake-from-sleep通知來幫助你關聯(lián)這些事件之間的時間。
由于定時器和其他周期性的事件在run loop運行時被傳遞,從而避免了循環(huán)在傳遞事件時被中斷。這種情況典型的例子就是你在進入run loop之后實現(xiàn)了一個鼠標追蹤回調,然后你會重復地從應用中收到該事件。由于你的代碼是直接抓取事件,而不是讓應用來常規(guī)地分發(fā)那些事件,活動的定時器并不會觸發(fā)直到退出鼠標追蹤回調之后并返回應用控制時。
使用run loop對象可以顯式地喚醒run loop。其他事件也有可能造成run loop的喚醒。如,加入其他的non-port-based輸入源會喚醒run loop以便使得輸入源(事件)能夠被立即處理,而不是等到事件發(fā)生時才這樣做。
何時使用Run Loop?
只有當你為應用創(chuàng)建輔助線程時才會需要顯式地運行一個run loop。應用程序主線程的run loop是一個十分關鍵的結構。因此,應用框架會提供主線程run loop自動開始的代碼支持。iOS中UIApplication(或者OS X中NSApplication)的run方法會啟動主線程run loop作為常規(guī)啟動隊列中的一步。如果你使用Xcode的項目模板來創(chuàng)建應用,你不必顯式地調用這些例程。
對于輔助線程而言,需要先決定run loop是否必須,如果是這樣,才去配置并啟動它。你不必在所有情況下為線程開啟run loop。例如,如果你使用線程做某個長期且預定的工作,也許可以避免開啟run loop。Run loop在更多情況下用在與線程做額外交互的??場景中。當你計劃做以下工作時你需要開啟run loop:
- 使用基于端口或自定義輸入源來和其他線程交流時。
- 線程上使用定時器時。
- 在Cocoa應用中使用任何
performSelector...相關的方法時。 - 保持線程執(zhí)行周期性的任務時。
如果你選擇使用run loop,其創(chuàng)建和配置是顯而易見的。正如所有線程編程技術一樣,你應該有一個在合適場景下退出輔助線程的計劃,干凈地退出線程總是比強制終止它更好。
使用Run Loop對象
Run loop對象提供了向其添加輸入源、定時器、觀察者并運行它的主接口。每個線程有單獨的一個run loop對象與其關聯(lián)。在Cocoa中,該對象作為NSRunLoop的實例。在低等級應用中,它是一個指向CFRunLoopRef類型的指針。
獲取Run Loop對象
為獲得當前線程的run loop,可以采取下列方式中的其中一種:
- 在Cocoa應用中,使用NSRunLoop的
currentRunLoop類方法獲取NSRunLoop對象。 - 使用
CFRunLoopGetCurrent函數(shù)。
盡管它們不是免費橋接的類型,當需要時還是可以從NSRunLoop對象中獲取CFRunLoopRef類型。NSRunLoop定義了getCFRunLoop方法以返回可傳遞給Core Foundation例程的CFRunLoopRef類型。由于兩種對象都是指向相同的run loop,你可以根據(jù)需求混合調用NSRunLoop對象和CFRunLoopRef類型。
配置Run Loop
在輔助線程上運行run loop之前,你必須為其添加至少一個輸入源或定時器。如果run loop沒有任何源來監(jiān)測,它會在你試圖啟動它時立即退出。
除了安裝源之外,你同樣可以安裝run loop觀察者并使用它們來檢測run loop執(zhí)行的不同階段。為創(chuàng)建run loop觀察者,你需要創(chuàng)建一個CFRunLoopObserverRef類型并使用CFRunLoopAddObserver函數(shù)將其添加到run loop中。Run loop觀察者必須使用Core Foundation創(chuàng)建,即便是Cocoa應用中。
代碼3-1展示了run loop中綁定觀察者的主例程代碼。該示例的目的在于展示如何創(chuàng)建run loop觀察者,所以代碼簡單地設置了一個監(jiān)測所有run loop事件的觀察者對象?;镜幕卣{例程(沒有顯示)被簡化用日志輸出的方式代替。
代碼3-1 創(chuàng)建run loop觀察者
- (void)threadMain
{
// 該應用使用垃圾回收處理,所以不需要自動釋放池
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// 創(chuàng)建觀察者并綁定到run loop
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer)
{
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
// 創(chuàng)建并調度定時器
[NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:@selector(doFireTimer:)
userInfo:nil
repeats:YES];
NSInteger loopCount = 10;
do
{
// 運行10次run loop后觸發(fā)定時器
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while (loopCount);
}
當為長周期線程配置run loop時,最好至少添加一個輸入源以接收消息。盡管你可以只綁定一個定時器就進入run loop,一旦定時器觸發(fā)完成它就會失效,這將導致該run loop退出。綁定一個重復性的定時器可以使run loop在比較長的周期內保持運行,但會涉及到定時喚醒線程,這會有效地作為另一種形式的輪詢定時器。相比之下,輸入源會等到事件發(fā)生前,線程都會保持睡眠。
啟動Run Loop
啟動run loop只針對應用中的輔助線程才是必須的操作。一個run loop必須至少有一個用于監(jiān)測的輸入源或定時器。如果沒有其中一個被綁定,run loop會立即退出。
有幾種啟動run loop的方式,大致如下:
- 無條件啟動。
- 限制時間內啟動。
- 在特定模式下啟動。
無條件地進入run loop是最簡單的選項,但這樣做最不可取。無條件地運行run loop會將線程置于固定的循環(huán)當中,這會讓你失去對run loop的控制。你可以添加或移除輸入源及定時器,但停止run loop的唯一方式是終止它。同樣在自定義模式下不能運行run loop。
除開無條件地運行run loop,最好設置一個超時時間后運行run loop。當設置時間閾值后,run loop會在事件到達或時間超時后運行。當事件到達,事件會被分發(fā)給處理回調然后run loop退出。你的代碼可以在之后重啟run loop以處理下一次事件。如果是分配時間過期,可以簡單地重新啟動run loop或重設時間來做任何需要的事務管理。
除了設置時間閾外,你同樣可以用指定的模式來運行run loop。模式和時間閾并不互斥并可以同時用于run loop啟動。模式會對傳遞事件的來源做出限制。
代碼3-2展示了線程主入口例程的框架版本。該示例的關鍵部分展示了run loop的基本結構。在本質上,你將輸入源和計時器添加到run loop,然后重復調用例程之一以啟動run loop。每次run loop例程返回時,請檢查是否出現(xiàn)了可能需要退出線程的任何條件。示例使用Core Foundation的run loop例程,以便它可以檢查返回結果,并確定run loop退出的原因。如果使用的是Cocoa方式且不需要檢查返回值,你也可以使用NSRunLoop類的方法以類似的方式運行run loop。(使用NSRunLoop類的方法完成run loop調用,示例參見代碼3-14。)
代碼3-2 運行run loop
- (void)skeletonThreadMain
{
// 如果不使用垃圾回收處理需設置一個自動釋放池
BOOL done = NO;
// 為run loop添加輸入源或定時器并做其他設置
do
{
// 啟動run loop并在每個源完成處理后返回
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
// 如果run loop被源顯式地停止,或者不存在輸入源或定時器,則退出run loop
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
{
done = YES;
}
// 檢查其他的退出條件并修改done變量
}
while (!done);
// 這里編寫清理代碼,確保釋放自動釋放池中的對象
}
遞歸地運行run loop是可能的情況。換句話說,你可以調用CFRunLoopRun,CFRunLoopRunInMode,或任何NSRunLoop方法從輸入源或定時器的回調例程內開始run loop。這樣做時,你可以使用任何想要運行嵌套run loop的模式,包括使用外部run loop的模式。
退出Run Loop
有兩種退出run loop的方式:
- 配置run loop運行超時時間閾值。
- 通知run loop退出。
使用超時的方式當然是首選,如果你能控制它。指定超時閾可以讓run loop結束所有的正常處理,包括在退出之前向run loop的觀察者傳遞通知。
使用CFRunLoopStop函數(shù)顯式地停止run loop產生的效果類似于超時。Run loop發(fā)出任何剩余的通知,然后退出。其區(qū)別在于,你可以在run loop無條件啟動的情況下使用這種技術。
雖然移除run loop的輸入源和定時器也可能導致其退出,但這不是退出run loop的可靠方式。一些系統(tǒng)例程將輸入源添加到run loop來處理所需事件。因為你的代碼可能不知道這些輸入源,所以無法刪除它們,這會阻止run loop退出。
線程安全與Run Loop對象
線程安全取決于使用哪個接口來操作你的run loop。Core Foundation上的功能通常是線程安全的,并且可以從任何線程調用。如果你正在執(zhí)行更改run loop配置的操作,那么,它仍然是很好的做法,這樣就可以在任何可能的時候都擁有run loop的線程了。
Cocoa的NSRunLoop類并不如它在Core Foundation中的副本(譯者注:CFRunLoopRef類型)那樣線程安全。如果你正在使用NSRunLoop類修改你的run loop,你應該只在同一個線程擁有的run loop這樣做。向屬于不同線程的run loop添加輸入源或定時器,可能導致你的代碼崩潰或表現(xiàn)異常。
配置Run Loop源
以下各節(jié)將展示Cocoa和Core Foundation中如何設置輸入源的類型。
定義自定義輸入源
創(chuàng)建自定義輸入源涉及到以下定義:
- 需要輸入源處理的信息。
- 讓感興趣的客戶端與輸入源交互的調度器例程。
- 處理客戶端請求回調的例程。
- 注銷輸入源的取消例程。
因為你創(chuàng)建了自定義的輸入源來處理自定義信息,所以實際的配置是靈活的。調度程序,處理程序和取消例程幾乎是所需自定義輸入源的全部關鍵例程。然而大多數(shù)輸入源的行為發(fā)生在這些處理程序以外的例程。例如,你定義了一個用于輸入源向其他線程傳遞數(shù)據(jù)和信息交換的機制。
圖3-2展示了一個自定義的輸入源配置示例。在這個例子中,應用程序的主線程維護著輸入源的引用,輸入源的自定義命令緩沖區(qū),以及安裝了輸入源的run loop。當主線程有一個需交由工作線程的任務時,它將向命令緩沖區(qū)發(fā)出命令以及工作線程所需要的任何信息來啟動任務。(由于主線程和工作線程的輸入源都可以訪問命令緩沖區(qū),所以訪問必須是同步的。)一旦命令被發(fā)出,主線程會向輸入源發(fā)出信號,并喚醒工作線程的run loop。在接收喚醒命令時,run loop調用該輸入源的處理程序,輸入源會處理在命令緩沖區(qū)中找到的命令。
圖3-2 自定義輸入源的操作流程
[圖片上傳失敗...(image-675d05-1536734662071)]
以下部分闡釋了自定義輸入源的實現(xiàn),并顯示了需要實現(xiàn)的關鍵代碼。
定義輸入源
定義自定義的輸入源需要使用Core Foundation的例程,以此來對run loop源進行配置和綁定。盡管基本的回調都是基于C語言的函數(shù),這并不意味著你需要為那些函數(shù)編寫Objective-C或C++的代碼包裝。
圖3-2中所示的輸入源使用Objective-C對象管理命令緩沖區(qū),并和run loop進行調節(jié)工作。代碼3-3展示了輸入源對象的定義。RunLoopSource對象管理著命令緩沖區(qū)并使用該緩沖區(qū)來接收其他線程的消息。該段代碼同樣展示了RunLoopContext對象的定義,該對象作為傳遞RunLoopSource對象的容器對象以及run loop對應用主線程的引用對象。
代碼3-3 自定義輸入源對象定義
@interface RunLoopSource : NSObject
{
CFRunLoopSourceRef runLoopSource;
NSMutableArray* commands;
}
- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;
// Handler method
- (void)sourceFired;
// Client interface for registering commands to process
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
@end
// These are the CFRunLoopSourceRef callback functions.
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
// RunLoopContext is a container object used during registration of the input source.
@interface RunLoopContext : NSObject
{
CFRunLoopRef runLoop;
RunLoopSource* source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;
- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end
雖然Objective-C代碼管理著輸入源的自定義數(shù)據(jù),將輸入源綁定到run loop需要基于C的回調函數(shù)。這些函數(shù)在run loop源綁定到run loop后有一次初次調用,并在代碼3-4展示。因為這個輸入源只有一個客戶端(主線程),它會調用調度函數(shù)來向線程發(fā)送一個注冊自身應用委托對象的消息。當委托對象希望與輸入源通信時,它會利用在RunLoopContext對象中的信息來完成。
代碼3-4 調度run loop源
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj
andLoop:rl];
[del performSelectorOnMainThread:@selector(registerSource:)
withObject:theContext
waitUntilDone:NO];
}
最重要的回調處理例程是輸入源被通知時處理自定義數(shù)據(jù)的例程。代碼3-5展示了該例程與RunLoopSource的關聯(lián)。該函數(shù)簡單地發(fā)起了一個名為sourceFired的方法的工作請求,sourceFired方法內部會處理命令緩沖池中存在的任何命令。
代碼3-5 輸入源中執(zhí)行任務
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}
如果你一旦調用CFRunLoopSourceInvalidate函數(shù)將輸入源從run loop中移除,系統(tǒng)會調用輸入源的取消例程。你可以在該例程中通知客戶端輸入源將要失效并讓其移除對輸入源的引用。代碼3-6展示了使用RunLoopSource對象注冊的取消回調例程。該函數(shù)向應用的委托對象發(fā)送了另一個RunLoopContext對象,但這次是讓請求對象移除對run loop源的引用。
代碼3-6 停止輸入源
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
[del performSelectorOnMainThread:@selector(removeSource:)
withObject:theContext
waitUntilDone:YES];
}
注意:應用委托對象的
registerSource:和removeSource:方法代碼在之前的代碼3-8中展示。
為Run Loop安裝輸入源
代碼3-7展示了RunLoopSource類的init和addToCurrentRunLoop方法。init方法創(chuàng)建了與run loop實際綁定的CFRunLoopSourceRef類型。它將RunLoopSource對象作為上下文信息傳遞以讓回調例程持有該對象的指針。輸入源的安裝直到工作線程調用addToCurrentRunLoop方法才發(fā)生,這時RunLoopSourceScheduleRoutine回調會被調用。一旦輸入源被添加到run loop,線程會運行run loop并在run loop上做好準備。
代碼3-7 安裝輸入源
- (id)init
{
CFRunLoopSourceContext context = {0, self, NULL, NULL, NULL, NULL, NULL,
&RunLoopSourceScheduleRoutine,
RunLoopSourceCancelRoutine,
RunLoopSourcePerformRoutine};
runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
commands = [[NSMutableArray alloc] init];
return self;
}
- (void)addToCurrentRunLoop
{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}
調節(jié)客戶端與輸入源
為了使輸入源有用,你需要對其操作并在其他線程上向其發(fā)出信號。當無事可做時輸入源會讓關聯(lián)的線程處于睡眠狀態(tài)。這需要在應用中的其他線程上知道該輸入源并如何和它通信。
其中的一種方式就是在輸入源初次安裝到run loop時通知客戶端輸入源將要發(fā)出注冊的請求。你可以為你的輸入源注冊盡可能多的客戶端,或者簡單地向中心對象注冊然后向感興趣的客戶端“售出”輸入源。代碼3-8展示了應用委托對象內定義的注冊方法,并在RunLoopSource對象的調度函數(shù)調用。該方法從RunLoopSource對象獲取RunLoopContext對象并將其添加到它的多個源當中。代碼也展示了將輸入源從run loop注銷的函數(shù)例程。
代碼3-8 使用應用委托對象完成輸入源注冊與移除
- (void)registerSource:(RunLoopContext*)sourceInfo;
{
[sourcesToPing addObject:sourceInfo];
}
- (void)removeSource:(RunLoopContext*)sourceInfo
{
id objToRemove = nil;
for (RunLoopContext* context in sourcesToPing)
{
if ([context isEqual:sourceInfo])
{
objToRemove = context;
break;
}
}
if (objToRemove)
{
[sourcesToPing removeObject:objToRemove];
}
}
注意:調用這些方法的回調函數(shù)代碼在之前的代碼3-4和代碼3-6中展示。
向輸入源發(fā)出信號
在向輸入源傳遞數(shù)據(jù)之后,客戶端必須向源發(fā)起信號并喚醒其run loop。向源發(fā)起信號的目的是讓run loop知道源已經準備好處理了。還有一個原因就是線程可能在發(fā)起信號時處理睡眠狀態(tài),你應該總是顯式地喚醒run loop。如果沒能完成這樣的操作,可能會導致輸入源延遲處理。
代碼3-9展示了RunLoop的fireCommandsOnRunLoop方法??蛻舳嗽谳斎朐醋龊脺蕚涮幚砭彌_區(qū)命令時調用該方法。
代碼3-9 喚醒run loop
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}
注意:你不能通過自定義輸入源嘗試處理
SIGHUP或其他類型的進程級信號。Core Foundation中喚醒run loop的函數(shù)并不安全且不應該在應用的信號處理例程中使用。
配置定時源
為創(chuàng)建定時器源,所有你需要做的就是創(chuàng)建一個定時器對象并完成它在run loop上的調用。在Cocoa中,可以使用NSTimer類創(chuàng)建新的定時器對象,在Core Foundation中則使用CFRunLoopTimerRef類型。本質上,NSTimer類是Core Foundation上提供的一種更加便利功能的擴展,如在同一個方法上創(chuàng)建并調度定時器的能力。
在Cocoa中,你可以用以下的類方法同時完成定時器的創(chuàng)建和調度:
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:scheduledTimerWithTimeInterval:invocation:repeats:
這些方法會創(chuàng)建定時器并將其添加到默認模式下的run loop中。(NSDefaultRunLoopMode)你也可以按照需求手動地調度定時器,這需要使用NSRunLoop的addTimer:forMode:方法來創(chuàng)建NSTimer對象并將其添加到run loop。這兩種技術都能夠完成基本的操作但給你對于定時器不同層級的控制。例如,如果創(chuàng)建定時器并手動地添加到run loop,則可以使用除默認模式外的其他模式。代碼3-10展示了如何使用這兩種技術來創(chuàng)建定時器。第一個定時器有1秒的創(chuàng)建延遲并在之后每隔0.1秒觸發(fā)一次。第二個定時器在初始化后的0.2秒觸發(fā)并在之后每隔0.2秒觸發(fā)一次。
代碼3-10 使用NSTimer創(chuàng)建并調度定時器
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// Create and schedule the first timer.
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
interval:0.1
target:self
selector:@selector(myDoFireTimer1:)
userInfo:nil
repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
// Create and schedule the second timer.
[NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(myDoFireTimer2:)
userInfo:nil
repeats:YES];
代碼3-11展示了使用Core Foundation函數(shù)對定時器完成配置的代碼。盡管該示例在上下文并不傳遞任何用戶定義的信息,你可以使用該上下文為定時器傳遞任何自定義的數(shù)據(jù)。
代碼3-11 使用Core Foundation創(chuàng)建并調度定時器
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
&myCFTimerCallback, &context);
CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
配置基于端口的輸入源
Cocoa和Core Foundation同時提供了用于線程或進程間通信的基于端口的對象。下面部分將向你展示如何使用不同類型的端口來實現(xiàn)端口通信。
配置NSMachPort對象
為了使用NSMachPort對象建立本地連接,你需要創(chuàng)建一個端口對象并添加到主要線程的run loop中。當啟動輔助線程后,向該線程的入口點函數(shù)傳遞相同的端口對象。輔助線程就可以使用這個對象來向主要線程發(fā)送消息。
主線程代碼實現(xiàn)
代碼3-12展示了如何啟動輔助的工作線程的主線程代碼。由于Cocoa框架實現(xiàn)了許多配置run loop端口的中介步驟,launchThread方法明顯比Core Foundation中等價的函數(shù)(代碼3-17)短;然而它們兩者行為近乎等同。唯一的區(qū)別在于該方法直接發(fā)送NSPort對象,而不是向工作線程發(fā)送本地端口的名字。
代碼3-12 主線程的啟動方法
- (void)launchThread
{
NSPort* myPort = [NSMachPort port];
if (myPort)
{
// This class handles incoming port messages.
[myPort setDelegate:self];
// Install the port as an input source on the current run loop.
[[NSRunLoop currentRunLoop] addPort:myPort
forMode:NSDefaultRunLoopMode];
// Detach the thread. Let the worker release the port.
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
toTarget:[MyWorkerClass class]
withObject:myPort];
}
}
為了實現(xiàn)線程間雙邊通信的信道,你應該想要工作線程在向主線程發(fā)送的檢入信息中包含它自己的本地端口。接收到該檢入信息能夠讓你的主線程知道輔助線程的啟動狀況以及讓你向其發(fā)送額外的信息。
代碼3-13展示了主線程中的handlePortMessage:方法。該方法在數(shù)據(jù)到達本地端口時調用。當檢入信息到達時,該方法為輔助線程從端口信息中獲取端口并將其存儲起來以供使用。
代碼3-13 處理Mach端口消息
#define kCheckinMessage 100
// Handle responses from the worker thread.
- (void)handlePortMessage:(NSPortMessage *)portMessage
{
unsigned int message = [portMessage msgid];
NSPort* distantPort = nil;
if (message == kCheckinMessage)
{
// Get the worker thread’s communications port.
distantPort = [portMessage sendPort];
// Retain and save the worker port for later use.
[self storeDistantPort:distantPort];
}
else
{
// Handle other messages.
}
}
輔助線程代碼實現(xiàn)
對于輔助工作線程,你必須為配置該線程并使用指定端口來和主線程完成信息的回復。
代碼3-14展示了該工作線程的設置代碼。在為線程創(chuàng)建自動釋放池后,該方法創(chuàng)建了一個工作者對象并驅動線程的執(zhí)行。工作對象的sendCheckinMessage:方法(代碼3-15展示)創(chuàng)建了一個工作線程的本地端口并發(fā)送檢入消息給主線程。
代碼3-14 啟動使用Mach端口的輔助線程
+(void)LaunchThreadWithPort:(id)inData
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
// Set up the connection between this thread and the main thread.
NSPort* distantPort = (NSPort*)inData;
MyWorkerClass* workerObj = [[self alloc] init];
[workerObj sendCheckinMessage:distantPort];
[distantPort release];
// Let the run loop process things.
do
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
while (![workerObj shouldExit]);
[workerObj release];
[pool release];
}
當使用NSMachPort時,本地及遠程的對象可以使用同樣的端口對象進行線程間的通信。換句話講,由線程創(chuàng)建的本地端口對象可以成為其他線程的遠程端口。
代碼3-15展示了輔助線程中的檢入例程。該方法設置了一個用于單邊通信的本地端口,并在之后向主線程回送了檢入信息。該方法使用從LaunchThreadWithPort:方法接收到的端口對象作為消息的目標對象。
代碼3-15 使用Mach端口發(fā)送檢入信息
// Worker thread check-in method
- (void)sendCheckinMessage:(NSPort*)outPort
{
// Retain and save the remote port for future use.
[self setRemotePort:outPort];
// Create and configure the worker thread port.
NSPort* myPort = [NSMachPort port];
[myPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
// Create the check-in message.
NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
receivePort:myPort
components:nil];
if (messageObj)
{
// Finish configuring the message and send it immediately.
[messageObj setMsgId:setMsgid:kCheckinMessage];
[messageObj sendBeforeDate:[NSDate date]];
}
}
配置NSMessagePort對象
為創(chuàng)建基于NSMessagePort對象的本地連接,你不能在線程間簡單地傳遞端口對象。遠程消息端口根據(jù)名稱獲得。Cocoa中需要本地端口注冊指定的名稱并將名稱傳遞給遠端的線程以便其獲得用于通信的合適的端口對象。代碼3-16展示了消息端口的創(chuàng)建與注冊流程。
代碼3-16 注冊一個消息端口
NSPort* localPort = [[NSMessagePort alloc] init];
// Configure the object and add it to the current run loop.
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
// Register the port using a specific name. The name must be unique.
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
name:localPortName];
Core Foundation配置基于端口輸入源
本部分將展示如何使用Core Foundation在主線程和工作線程之間設置一個雙邊通信的信道。
代碼3-17展示了主線程啟動工作線程的調用代碼。該段代碼首先創(chuàng)建了一個CFMessagePortRef類型來監(jiān)聽工作線程上的消息。由于工作線程需要端口的名稱來建立連接,所以工作線程的入口點函數(shù)需要傳入字符串值。端口名稱在當前用戶的環(huán)境下必須是唯一的;否則可能會遇到沖突。
代碼3-17 為新線程綁定Core Foundation消息端口
#define kThreadStackSize (8 *4096)
OSStatus MySpawnThread()
{
// Create a local port for receiving responses.
CFStringRef myPortName;
CFMessagePortRef myPort;
CFRunLoopSourceRef rlSource;
CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
Boolean shouldFreeInfo;
// Create a string with the port name.
myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));
// Create the port.
myPort = CFMessagePortCreateLocal(NULL, myPortName,
&MainThreadResponseHandler,
&context,
&shouldFreeInfo);
if (myPort != NULL)
{
// The port was successfully created.
// Now create a run loop source for it.
rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (rlSource)
{
// Add the source to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
// Once installed, these can be freed.
CFRelease(myPort);
CFRelease(rlSource);
}
}
// Create the thread and continue processing.
MPTaskID taskID;
return(MPCreateTask(&ServerThreadEntryPoint, (void*)myPortName,
kThreadStackSize, NULL, NULL, NULL, 0, &taskID));
}
在端口安裝完成和線程啟動之后,主線程可以繼續(xù)它的常規(guī)執(zhí)行以等待線程的檢入。當檢入消息到達時,它會被分發(fā)到主線程的MainThreadResponseHandler函數(shù),在代碼3-18中顯示。該函數(shù)從工作線程中提取端口的名稱并創(chuàng)建好將來通信的渠道。
代碼3-18 接收檢入消息
#define kCheckinMessage 100
// Main thread port message handler
CFDataRef MainThreadResponseHandler(CFMessagePortRef local,
SInt32 msgid,
CFDataRef data,
void* info)
{
if (msgid == kCheckinMessage)
{
CFMessagePortRef messagePort;
CFStringRef threadPortName;
CFIndex bufferLength = CFDataGetLength(data);
UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0);
CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
threadPortName = CFStringCreateWithBytes (NULL, buffer, bufferLength, kCFStringEncodingASCII, FALSE);
// You must obtain a remote message port by name.
messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName);
if (messagePort)
{
// Retain and save the thread’s comm port for future reference.
AddPortToListOfActiveThreads(messagePort);
// Since the port is retained by the previous function, release
// it here.
CFRelease(messagePort);
}
// Clean up.
CFRelease(threadPortName);
CFAllocatorDeallocate(NULL, buffer);
}
else
{
// Process other messages.
}
return NULL;
}
完成主線程的配置后,唯一剩下的事情就是對新創(chuàng)建工作線程創(chuàng)建它自己的端口并檢入。代碼3-19展示了工作線程的入口函數(shù)。該函數(shù)從主線程中提取到端口名并使用它來創(chuàng)建回連主線程的端口。該函數(shù)然后創(chuàng)建了自己的安裝在run loop上的一個本地端口,并向主線程發(fā)送一條包含本地端口名的檢入消息。
代碼3-19 創(chuàng)建線程結構
OSStatus ServerThreadEntryPoint(void* param)
{
// Create the remote port to the main thread.
CFMessagePortRef mainThreadPort;
CFStringRef portName = (CFStringRef)param;
mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
// Free the string that was passed in param.
CFRelease(portName);
// Create a port for the worker thread.
CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID());
// Store the port in this thread’s context info for later reference.
CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
Boolean shouldFreeInfo;
Boolean shouldAbort = TRUE;
CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&ProcessClientRequest,
&context,
&shouldFreeInfo);
if (shouldFreeInfo)
{
// Couldn't create a local port, so kill the thread.
MPExit(0);
}
CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (!rlSource)
{
// Couldn't create a local port, so kill the thread.
MPExit(0);
}
// Add the source to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
// Once installed, these can be freed.
CFRelease(myPort);
CFRelease(rlSource);
// Package up the port name and send the check-in message.
CFDataRef returnData = nil;
CFDataRef outData;
CFIndex stringLength = CFStringGetLength(myPortName);
UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);
CFStringGetBytes(myPortName, CFRangeMake(0,stringLength),
kCFStringEncodingASCII, 0, FALSE, buffer, stringLength, NULL);
outData = CFDataCreate(NULL, buffer, stringLength);
CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL, NULL);
// Clean up thread data structures.
CFRelease(outData);
CFAllocatorDeallocate(NULL, buffer);
// Enter the run loop.
CFRunLoopRun();
}
一旦它進入run loop,所有將來發(fā)向該線程端口的事件都將由ProcessClientRequest函數(shù)處理。該函數(shù)的實現(xiàn)依賴于線程的具體工作且不在這里做展示。
線程同步
在應用程序中多線程的存在下,多線程的執(zhí)行產生了安全訪問資源的潛在問題。兩個線程可能不經意間就會修改相同的資源。例如,一個線程可能覆蓋另一個線程的更改,或者將應用程序置于一個未知的和潛在的無效狀態(tài)。如果你比較幸運,損壞的資源可能會導致明顯的性能問題或崩潰,這比較容易跟蹤和修復。但是如果你比較不幸,這導致的微小錯誤并不能即時被察覺或錯誤需要代碼修改多處才能解決。
說到線程安全,一個好的設計才是最好的保護方式。避免資源共享和減少線程之間的交互,使這些線程間不太那么容易相互干擾。然而一個完全避免干擾的設計是不可能的。在線程必須進行交互的前提下,你需要使用同步工具來保證它們之間的交互是安全的。
OS X和iOS提供眾多的同步工具供你使用,工具從提供互斥訪問到事件隊列。下面的章節(jié)將介紹這些工具以及如何在代碼中使用它們,以解決程序資源安全訪問的問題。
同步工具
為了防止線程超出預期的修改數(shù)據(jù),可以對應用的設計做出修改或者使用同步工具避免同步問題。雖然完全避免同步問題是可取的,但它并不總是可能的。下面的章節(jié)描述了可供你使用的同步工具的基本類別。
原子操作
原子操作是作用于簡單數(shù)據(jù)類型上的簡單同步形式。其優(yōu)勢在于它不阻塞相互競爭的線程。對于簡單的操作,譬如增加計數(shù)器變量,它比鎖的性能更加優(yōu)越。
OS X和iOS包括了多個用于32位及64位基本數(shù)學運算和邏輯運算的操作。在這些操作中屬于原子性的有compare-and-swap、test-and-set,以及test-and-clear。請查看/usr/include/libkern/OSAtomic.h頭文件或查看atomic的man幫助頁獲取原子操作支持的列表。
內存屏障與Volatile變量
為達到最優(yōu)性能,編譯器時常記錄匯編級別的指令以使處理器能盡可能滿載地處理流水線指令。作為該優(yōu)化的一部分,編譯器會記錄那些不會造成錯誤數(shù)據(jù)的主內存訪問指令。不幸的是,編譯器不能總是檢測到所有的內存獨立操作。如果看似獨立的變量在實際上卻相互影響,編譯器就會以錯誤的順序對其更新,產生潛在的錯誤結果。
內存屏障作為一種非阻塞式的同步工具,其用于確保內存操作處于正確順序。內存屏障的角色近乎于“柵欄”,它會強制處理器完成任何位于它之前載入和存儲的操作,而將其后的操作阻擋在外。內存屏障典型地應用于確保線程上內存操作(操作結果對其他線程可見)總是按照預定順序執(zhí)行。如果在剛才的情形下缺少了內存屏障,這將導致其他線程得到不可預計的結果。(譯者注:這里說的情形是讀寫者問題)為使用內存屏障,你可以在代碼合適的位置簡單地調用OSMemoryBarrier函數(shù)。
Volatile變量應用于對于單獨變量的內存限制。編譯器優(yōu)化代碼時常通過將變量的值載入寄存器中。對于本地變量,這通常沒有什么問題。如果一個變量對于其他線程可見,那么這樣的優(yōu)化也許會對其他線程對于該變量值修改的觀察造成影響。對變量使用volatile關鍵字可以強制編譯器每次使用變量時從內存中載入。你可以在變量可能任何時候被外部修改且編譯器不能檢測到時考慮聲明變量為volatile。
鎖
鎖作為最為常用的同步工具。你可以使用鎖來保護代碼的臨界區(qū),臨界區(qū)僅能在同一時刻被單個線程訪問。例如,臨界區(qū)在同一時刻可能會操作一個特殊的數(shù)據(jù)結構或者占用只支持一個用戶使用的資源。在該區(qū)域加入鎖,你就可以排除其他線程造成的修改和對代碼正確性造成影響。
表4-1列舉了程序猿常用的鎖方式。OS X和iOS提供了其中大多數(shù)鎖的實現(xiàn),但并不是全部。對于不支持的鎖類型,描述字段會解釋其未在該平臺直接實現(xiàn)的原因。
表4-1 鎖類型
| 鎖類型 | 描述 |
|---|---|
| 互斥鎖(Mutex) | 互斥排他(或互斥)鎖作為資源的保護屏障?;コ怄i是一種信號量類型并在同一時刻只為一個線程授權訪問資源。如果使用了互斥鎖然而其他線程試圖請求它,那個線程會阻塞直到互斥鎖被它原先的持有者釋放。如果多個線程競爭同一個互斥鎖,只有其中一個在同一時刻才有權擁有它。 |
| 遞歸鎖(Recursive lock) | 遞歸鎖是互斥鎖的一個變種。遞歸鎖允許同一個線程在其釋放前多次請求它。其他的線程則會阻塞直到鎖的持有者以和請求次數(shù)相同的釋放次數(shù)釋放該鎖。遞歸鎖主要用于遞歸操作,但也可以用于多個方法間獨立地請求該鎖。 |
| 讀寫鎖(Read-write lock) | 讀寫鎖可以被視作共享的排他鎖。該類型的鎖典型地應用于大批量的操作以及被保護數(shù)據(jù)讀操作頻繁而寫操作稀少的場景中。正常操作情況下,大量的讀者可以同時訪問數(shù)據(jù)。然而當線程想寫數(shù)據(jù)時,它直到讀者釋放鎖前都會阻塞,只有請求到該鎖時才能進行數(shù)據(jù)更新的操作。當寫入線程等待鎖時,新的讀者線程會阻塞直到寫線程完成。系統(tǒng)僅支持POSIX線程使用讀寫鎖。 |
| 分布式鎖(Distributed lock) | 分布式鎖提供進程級別的互斥訪問。不像真正的互斥。分布式鎖不阻塞進程的運行。它僅報告鎖很忙并讓進程決定如何執(zhí)行。 |
| 自旋鎖(Spin lock) | 自旋鎖在條件滿足前會不斷地輪詢獲得鎖。自旋鎖常用于預計等待鎖的時間短的多處理器系統(tǒng)中。在這樣的前提下,輪詢得到鎖通常比阻塞線程更加高效,因為后者涉及到線程狀態(tài)的切換和線程數(shù)據(jù)的更新。由于其輪詢的特性,系統(tǒng)不直接提供自旋鎖的實現(xiàn),但你可以在特定環(huán)境中簡單地實現(xiàn)。(譯者注:已經不建議最新系統(tǒng)中使用OSSpinLock,了解更多信息請移步這里) |
| 雙重檢查鎖(Double-checked lock) | 雙重檢查鎖為減少持有鎖帶來的資源消耗,它總是在持有鎖前先測試鎖的臨界條件。由于雙重檢查鎖有潛在的安全問題,系統(tǒng)并不顯式地提供支持且不鼓勵使用它。 |
注意:大多數(shù)類型的鎖也包含了內存屏障區(qū),以確保進入臨界區(qū)前完成先前需要完成的指令。
條件量
條件量作為另一種信號量類型以允許線程在某種條件滿足時相互通知。條件量常用于預示資源的可用性或者確保任務以特定順序執(zhí)行。當線程在測試條件時,它會一直阻塞直到條件為真。同樣地,直到其他線程顯式地修改條件前也會保持阻塞狀態(tài)。條件量與互斥鎖的區(qū)別在于多個線程能夠同時訪問鎖。條件量更像一個守門員一樣,它根據(jù)某些特定的條件讓不同的線程通過這道“門”。
一種可能使用條件量的情形是管理一系列掛起的事件。事件隊列需要使用一個條件變量來通知隊列中有等待事件的線程。如果事件到達,隊列會正確地喚起條件量。如果線程仍在等待,它會因為需要將事件放入隊列并處理而被喚醒。如果兩個事件幾乎同時進入隊列,隊列會通知條件量兩次并喚起兩個線程。
系統(tǒng)以幾種不同的技術為條件量提供支持。條件量的實現(xiàn)需要仔細的代碼編寫,所以你需要在編寫自己的代碼前查看后面的示例代碼。
Perform Selector
Cocoa應用有以同步形式向單個線程傳遞信息的便利方式。NSObject類聲明了在應用活動線程上執(zhí)行選擇器的一些方法。這些方法使你的線程異步地傳遞信息并保證在目標線程上同步地執(zhí)行。例如,你可能使用這種方式向應用的主線程或者指定線程傳遞分布式運算的結果。每次執(zhí)行選擇器的請求都被存儲在線程run loop的事件隊列中,并且請求會按照他們被收到時的順序線性地執(zhí)行。
同步消耗與性能
同步助你確保代碼的正確性,但那樣做確實損耗不少性能。即便在無沖突的情況下,同步工具都不是首選。鎖和原子操作通常涉及到內存屏障和內核級同步過程的使用來確保代碼受到正確保護。如果出現(xiàn)了鎖的沖突,你的線程會阻塞并經歷更嚴重的延遲。
表4-2列舉了使用互斥和原子操作的近似消耗。這些數(shù)據(jù)作為從幾千份樣本中采集的平均值。由于考慮了線程的創(chuàng)建時間,互斥鎖獲取時間會根據(jù)處理器的負載、計算機速度、系統(tǒng)內存和程序內存而千差萬別。
表4-2 互斥和原子操作資源消耗
| 指標 | 近似消耗 | 注釋 |
|---|---|---|
| 互斥鎖獲取時間 | 接近0.2微秒 | 這是在無沖突的情況下鎖的獲取時間。如果鎖被另一個線程持有,獲取時間還會更多。該數(shù)據(jù)由平臺上(基于Intel處理器的iMac/2GHz雙核處理器/1G內存/OS X 10.5系統(tǒng))分析平均值和中位值測定的數(shù)據(jù)決定。 |
| 原子性的compare-and-swap | 接近0.05微秒 | 這是在無沖突的情況下執(zhí)行compare-and-swap操作的花費時間。該數(shù)據(jù)由平臺上(基于Intel處理器的iMac/2GHz雙核處理器/1G內存/OS X 10.5系統(tǒng))分析平均值和中位值測定的數(shù)據(jù)決定。 |
當你設計并發(fā)任務時,正確性才通常是最為重要的考慮因素,但是你也應該考慮性能因素。代碼在多線程的方式下正確執(zhí)行,但是卻比單線程執(zhí)行更慢,這就難說是一種改進了。
如果你正在對一個現(xiàn)有的單線程應用程序進行改造,你應該經常設置一系列關鍵任務的性能基線測量值。在添加附加的線程后,你應該對于相同的任務收集新的數(shù)據(jù)并比較單線程和多線程的性能差異。如果在調整代碼后,線程并沒有提高性能,你可能希望重新考慮你的具體實現(xiàn)或使用線程的方式問題。
更多關于性能和數(shù)據(jù)收集工具的信息,請看[譯]性能概述。
線程安全與信號量
當遇到多線程應用,沒有什么比處理信號量更讓人恐懼和困惑的了。信號量是一種低等級的BSD機制以便能用于傳輸給信息進程或以某種方式處理信息。一些程序使用信號量來檢測特定事件,比如子進程的死亡。系統(tǒng)會使用信號量來終止失去控制的進程以及傳遞其他類型的信息。
信號量的問題并不在它們做的是什么,而是應用有多個線程時它們的行為。在單線程應用中,所有的信號量處理回調都在主線程上。在多線程應用中,不依賴于任何硬件錯誤的(如非法指令)的信號被傳遞到任何一個線程的合適時機下運行。如果多個線程同時運行,信號量會被傳遞到系統(tǒng)所選的任何一個線程。換句話說,信號量可以被傳遞到應用中的任何線程。
在應用程序中實現(xiàn)信號處理回調的首要規(guī)則是避免線程處理該信號的假設。如果某個特定的線程需要處理給定的信號,則需要編寫在信號到達時通知該線程的一些方法。你不能只假設線程中信號處理回調的安裝會讓該信號被傳遞到這個線程。
獲取更多關于信號量和信號量回調安裝的信息,請查看signal和sigaction的man幫助頁面。