[譯]線程編程指南(二)

由于文章長(zhǎng)度限制,本文作為[譯]線程編程指南(一)后續(xù)部分。

Run Loops

Run loop是與線程相關(guān)的基礎(chǔ)結(jié)構(gòu)之一。Run loop是一個(gè)用來(lái)調(diào)度工作并接收事件的事件處理循環(huán)。Run loop其目的在于有任務(wù)的時(shí)候讓線程保持忙碌狀態(tài)而無(wú)事可做時(shí)讓線程睡眠。

Run loop管理并不是完全自動(dòng)的。你還必須設(shè)計(jì)出線程代碼在適當(dāng)?shù)臅r(shí)間啟動(dòng)run loop,并響應(yīng)傳入的事件。Cocoa和Core Foundation提供run loop對(duì)象以幫助你配置和管理線程的run loop。應(yīng)用程序不需要顯式地創(chuàng)建這些對(duì)象;每一個(gè)線程,包括應(yīng)用程序的主線程,都有一個(gè)相關(guān)的run loop對(duì)象。只有輔助線程才需要顯式地運(yùn)行它們的run loop。應(yīng)用程序會(huì)自動(dòng)設(shè)置和運(yùn)行主線程的run loop并將其作為程序啟動(dòng)過(guò)程的一部分。

以下各節(jié)提供有關(guān)run loop的更多信息,以及如何在應(yīng)用程序中配置它們。

剖析Run Loop

Run loop正如其名稱一樣,它是一個(gè)讓線程進(jìn)入并用來(lái)運(yùn)行事件回調(diào)的循環(huán)。你的代碼提供的控制語(yǔ)句會(huì)用于實(shí)現(xiàn)run loop中的實(shí)際循環(huán)部分,換句話說(shuō),你的代碼中的whilefor循環(huán)驅(qū)動(dòng)著run loop的運(yùn)行。在run loop內(nèi),你將使用run loop對(duì)象來(lái)驅(qū)動(dòng)事件處理代碼執(zhí)行以接收事件和調(diào)用處理回調(diào)。

Run loop通過(guò)兩種源來(lái)接收事件。輸入源傳遞異步的事件,通常是其他線程和另一個(gè)應(yīng)用傳入。定時(shí)源傳遞同步事件,發(fā)生在調(diào)度時(shí)期或者重復(fù)周期。**當(dāng)相關(guān)事件到達(dá)時(shí)兩種類型的源都會(huì)使用應(yīng)用指定的回調(diào)例程來(lái)處理事件。

圖3-1展示了run loop及其不同類型源的概念結(jié)構(gòu)。輸入源傳遞異步事件給相應(yīng)的回調(diào)并引起runUntilDate:方法調(diào)用(實(shí)際由線程的NSRunLoop對(duì)象調(diào)用)以退出run loop。定時(shí)源傳遞事件給回調(diào)例程但并不導(dǎo)致run loop退出。

圖3-1 Run loop及其源結(jié)構(gòu)

[圖片上傳失敗...(image-725270-1536734662071)]

除了處理輸入源之外,run loop同樣會(huì)為其行為創(chuàng)建通知。注冊(cè)一個(gè)run loop的觀察者可以獲取到這些通知并使用它們?cè)诰€程中做進(jìn)一步的操作。

以下章節(jié)將提供有關(guān)run loop構(gòu)成和該模式如何操作的更多細(xì)節(jié)。同樣會(huì)描述事件處理的不同階段通知的創(chuàng)建過(guò)程。

Run Loop模式

一個(gè)run loop的模式包括了一系列待監(jiān)測(cè)的輸入源、定時(shí)源以及待通知的觀察者。每次運(yùn)行run loop時(shí),都會(huì)(顯式或隱式地)為其指定一個(gè)運(yùn)行“模式”。在run loop過(guò)程中,只監(jiān)測(cè)與該模式相關(guān)聯(lián)的源,并允許相關(guān)的事件傳遞。(類似地,只有與該模式相關(guān)聯(lián)的觀察者通知run loop執(zhí)行)與其他模式相關(guān)聯(lián)來(lái)源于任何新事件的消息,直到隨后在適當(dāng)模式下才進(jìn)入循環(huán)。

在代碼當(dāng)中,使用名稱來(lái)定義模式。Cocoa和Core Foundation定義了默認(rèn)模式和幾個(gè)常用模式,在代碼中都是以字符串的形式表示。你也可以定義一個(gè)自定義模式并用字符串為模式命名。雖然自定義模式的名稱是任意的,但模式的內(nèi)容卻不是。你必須確認(rèn)為其加入一個(gè)或多個(gè)輸入源、定時(shí)源、觀察者才會(huì)使其產(chǎn)生真正的效果。

你可以使用模式來(lái)過(guò)濾掉run loop中不感興趣的事件。多數(shù)情況下,你會(huì)希望run loop運(yùn)行在系統(tǒng)默認(rèn)定義的“default”模式下。對(duì)于“模態(tài)面板”,可能會(huì)運(yùn)行在“模態(tài)”模式下。當(dāng)進(jìn)入該模式時(shí),只有源相關(guān)的模態(tài)面板才能在線程中傳遞事件。對(duì)于輔助線程而言,你需要使用自定義模式來(lái)防止低優(yōu)先級(jí)源在時(shí)間敏感的操作中傳遞事件。

注意:模式基于事件的源區(qū)分而不是事件類型。例如,你不能使用模式來(lái)僅匹配鼠標(biāo)點(diǎn)擊事件或者鍵盤事件。你應(yīng)該使用模式來(lái)監(jiān)聽不同的端口,偶爾掛起定時(shí)器,或者切換當(dāng)前被監(jiān)測(cè)源和觀察者。

表3-1列舉了Cocoa和Core Foundation中的標(biāo)準(zhǔn)模式及其如何使用的描述。名稱字段列舉了在代碼中使用該模式的名稱常量。

表3-1 預(yù)定義的run loop模式

模式 名稱 描述
默認(rèn)(Default) NSDefaultRunLoopMode (Cocoa)、kCFRunLoopDefaultMode (Core Foundation) 大多數(shù)操作使用的默認(rèn)模式。大多數(shù)時(shí)間,你會(huì)使用該模式啟動(dòng)run loop并配置輸入源。
連接(Connection) NSConnectionReplyMode (Cocoa) Cocoa使用該模式來(lái)連接NSConnection對(duì)象以監(jiān)視其回應(yīng)。你自身很少會(huì)用到這種模式。
模態(tài)(Modal) NSModalPanelRunLoopMode (Cocoa) Cocoa使用該模式來(lái)追蹤相關(guān)模態(tài)面板的事件。
事件追蹤(Event tracking) NSEventTrackingRunLoopMode (Cocoa) Cocoa使用該模式來(lái)限制其他事件進(jìn)入當(dāng)鼠標(biāo)拖動(dòng)期間以及其他用戶界面跟蹤期間。
通用模式(Common modes NSRunLoopCommonModes (Cocoa)、kCFRunLoopCommonModes (Core Foundation) 這是一個(gè)可配置的通用模式組。為其中的一個(gè)模式關(guān)聯(lián)輸入源的同時(shí)會(huì)為該組中的所有模式關(guān)聯(lián)該輸入源。對(duì)于Cocoa應(yīng)用,這個(gè)集合默認(rèn)包含了上述全部模式。對(duì)于Core Foundation來(lái)講則最初只包含了默認(rèn)模式。你可以使用CFRunLoopAddCommonMode函數(shù)向其添加自定義模式。
輸入源

輸入源在你的線程中異步地傳遞事件。事件的來(lái)源取決于輸入源的類型,通常是兩種中的一個(gè)類型。基于端口的輸入源在你應(yīng)用的Mach端口上進(jìn)行監(jiān)聽。自定義的輸入源監(jiān)聽自定義的事件源。只要是涉及到run loop,一個(gè)輸入源是基于端口的或是自定義的并不重要。系統(tǒng)通常會(huì)實(shí)現(xiàn)兩種類型以供使用。這兩種源唯一區(qū)別在于它們通知方式不同?;诙丝诘脑从蓛?nèi)核自動(dòng)通知,自定義的源需要從其他線程手動(dòng)通知。

當(dāng)你創(chuàng)建完輸入源之后,可以將其指定到run loop的一個(gè)或多個(gè)模式下。在任何時(shí)刻模式都會(huì)影響到輸入源的受監(jiān)視情況。在大多數(shù)時(shí)間,你會(huì)在默認(rèn)模式下運(yùn)行run loop,但是你仍可以指定到自定義模式下運(yùn)行。如果輸入源不在當(dāng)前監(jiān)視的模式下,任何它產(chǎn)生的事件會(huì)被保留直到下一次run loop運(yùn)行到正確的模式。

以下具體描述了各種類型的輸入源。

基于端口的輸入源

Cocoa和Core Foundation提供了使用基于端口的對(duì)象和函數(shù)來(lái)創(chuàng)建基于端口輸入源的內(nèi)建支持。例如,在Cocoa中,你不必直接創(chuàng)建輸入源。你僅需使用NSPort的方法創(chuàng)建端口對(duì)象并將該端口添加到run loop。端口對(duì)象會(huì)為你處理創(chuàng)建和配置所需的輸入源。

在Core Foundation中,你必須同時(shí)手動(dòng)創(chuàng)建端口和run loop源。在這種情況下,你可以使用端口相關(guān)的不透明類型(CFMachPortRef,CFMessagePortRef,或CFSocketRef)的函數(shù)來(lái)創(chuàng)建合適的對(duì)象。

自定義輸入源

為創(chuàng)建自定義輸入源,你必須使用Core Foundation中CFRunLoopSourceRef類型相關(guān)的函數(shù),并使用多個(gè)不同的回調(diào)函數(shù)來(lái)配置自定義的輸入源。Core Foundation調(diào)用這些函數(shù)來(lái)配置源,處理任何到達(dá)事件,以及當(dāng)源從run loop移除時(shí)銷毀它們。

當(dāng)事件到達(dá)時(shí),定義自定義源的行為的同時(shí),你必須定義事件的傳遞機(jī)制。源的這部分運(yùn)行在一個(gè)單獨(dú)的線程中,負(fù)責(zé)為輸入源提供數(shù)據(jù),并在數(shù)據(jù)準(zhǔn)備好時(shí)發(fā)出信號(hào)。事件傳遞機(jī)制是取決于需求而不必過(guò)于復(fù)雜。

Cocoa執(zhí)行選擇器源

除了基于端口的輸入源之外,Cocoa定義了一種可以允許在任何線程上執(zhí)行選擇器的自定義輸入源。同基于端口的輸入源類似,執(zhí)行選擇器的請(qǐng)求被序列化到目標(biāo)線程,緩解了同一線程上運(yùn)行的多個(gè)方法間可能存在的同步問(wèn)題的產(chǎn)生。與基于端口的源不同的是,執(zhí)行選擇器的源會(huì)在執(zhí)行完選擇器之后將自身從run loop上移除。

注意:早在OS X 10.5之前,執(zhí)行選擇器源被主要用于主線程的消息發(fā)送上,但在OS X 10.5之后以及iOS上,你可以使用它們向任何線程發(fā)送消息。

當(dāng)在另一個(gè)線程上執(zhí)行選擇器時(shí),目標(biāo)線程必須有一個(gè)活動(dòng)的run loop。這意味著線程一旦創(chuàng)建,你的代碼就必須顯式地開啟一個(gè)run loop。由于主線程會(huì)開啟它自身的run loop,然而,你也可以在應(yīng)用的委托回調(diào)方法applicationDidFinishLaunching:完成該調(diào)用。Run loop會(huì)在每次通過(guò)循環(huán)時(shí)完成所有隊(duì)列中的選擇器調(diào)用,而不是在每次循環(huán)中只處理一個(gè)。

表3-2列舉了NSObject中可用于在其他線程上執(zhí)行選擇器的方法。由于這些方法都由NSObject聲明,所以你能夠在任何能訪問(wèn)到Objective-C對(duì)象的線程上使用它們,包括POSIX線程。這些方法事實(shí)上不會(huì)創(chuàng)建新的線程來(lái)執(zhí)行選擇器。

表3-2 在其他線程上執(zhí)行選擇器

名稱 描述
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在應(yīng)用主線程的下一個(gè)run loop周期內(nèi)執(zhí)行指定的選擇器。該方法會(huì)阻塞當(dāng)前線程直到選擇器執(zhí)行完畢。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在任何一個(gè)NSThread對(duì)象的線程上執(zhí)行指定選擇器。該方法會(huì)阻塞當(dāng)前線程直到選擇器執(zhí)行完畢。
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在當(dāng)前線程的下一個(gè)run loop周期內(nèi)的選擇性延遲時(shí)間后執(zhí)行指定的選擇器。由于它會(huì)等到下一個(gè)run loop的周期才執(zhí)行,所以該方法提供了一個(gè)從當(dāng)前起自動(dòng)的最小延遲執(zhí)行時(shí)間。隊(duì)列中的多個(gè)選擇器在排列好后會(huì)依次執(zhí)行。
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
取消上一個(gè)執(zhí)行請(qǐng)求。
定時(shí)源

定時(shí)源在將來(lái)的一個(gè)預(yù)定時(shí)間向線程同步地傳遞事件。定時(shí)器作為線程通知自身完成某些工作的一種方式。例如,搜索框一旦在用戶連續(xù)輸入關(guān)鍵字間隙可以使用計(jì)時(shí)器來(lái)發(fā)起自動(dòng)搜索。在開始搜索之前,搜索操作的延時(shí)可以讓用戶盡可能多的輸入所需的關(guān)鍵字符串。

盡管定時(shí)源創(chuàng)建了基于時(shí)間的通知,但定時(shí)器并不是一個(gè)實(shí)時(shí)機(jī)制。類似輸入源,定時(shí)器與run loop的具體模式關(guān)聯(lián)。如果定時(shí)器在run loop模式中當(dāng)前沒有處于被監(jiān)控狀態(tài),它會(huì)直到run loop運(yùn)行到支持的模式下才開啟。同理,如果定時(shí)器在run loop執(zhí)行到回調(diào)中是觸發(fā),該定時(shí)器會(huì)等到下一次進(jìn)入run loop時(shí)才回去調(diào)用回調(diào)。如果run loop根本就沒有運(yùn)行,定時(shí)器永遠(yuǎn)不會(huì)被觸發(fā)。

你可以創(chuàng)建定時(shí)器以一次或者重復(fù)多次地產(chǎn)生事件。重復(fù)性的定時(shí)器會(huì)自動(dòng)基于其調(diào)度觸發(fā)時(shí)間重新調(diào)度,而并不是實(shí)際的觸發(fā)時(shí)間。例如,如果一個(gè)定時(shí)器原定于每5秒完成一次觸發(fā),調(diào)度觸發(fā)時(shí)間總是落在原有的5秒間隔上,即便實(shí)際的觸發(fā)時(shí)間產(chǎn)生了延遲。如果觸發(fā)時(shí)間被延遲過(guò)多則會(huì)導(dǎo)致其錯(cuò)過(guò)一次或多次的調(diào)度觸發(fā)機(jī)會(huì),使得定時(shí)器在某一段時(shí)間內(nèi)只觸發(fā)了一次。在調(diào)度丟失的時(shí)間完成觸發(fā)之后,定時(shí)器會(huì)在下一次調(diào)度觸發(fā)時(shí)間重新調(diào)度。

Run Loop觀察者

對(duì)比那些在合適時(shí)間觸發(fā)或同步或異步的事件的源,run loop觀察者則在run loop自身執(zhí)行的特定位置觸發(fā)事件。你可以使用run loop觀察者對(duì)線程的既定事件做準(zhǔn)備或者在線程睡眠之前做好準(zhǔn)備。你可以在run loop中為觀察者關(guān)聯(lián)如下事件:

  • Run loop進(jìn)入。
  • Run loop將要處理定時(shí)器。
  • Run loop將要處理輸入源。
  • Run loop將要睡眠。
  • Run loop將要喚醒,但在其處理事件之前。
  • Run loop退出。

你可以使用Core Foundation提供的方式來(lái)為應(yīng)用創(chuàng)建run loop觀察者。為創(chuàng)建run loop觀察者,你需要?jiǎng)?chuàng)建CFRunLoopObserverRef類型的實(shí)例。該類型會(huì)追蹤自定義的回調(diào)函數(shù)以及感興趣的活動(dòng)。

和定時(shí)器一樣,觀察者可以被一次性或重復(fù)性的使用。一次性觀察者在觸發(fā)之后會(huì)將自己從run loop中移除,而重復(fù)執(zhí)行的觀察者則繼續(xù)保留。你可以在觀察者創(chuàng)建時(shí)指定其是一次性的還是重復(fù)性的。

Run Loop事件隊(duì)列

在每次run loop運(yùn)行時(shí),線程的run loop執(zhí)行掛起事件并為任何關(guān)聯(lián)的觀察者創(chuàng)建通知。這些事件的通知順序如下:

  1. 通知觀察者run loop進(jìn)入。
  2. 通知觀察者就緒的定時(shí)器即將觸發(fā)。
  3. 通知觀察者non-port-based(非基于端口)輸入源即將觸發(fā)。
  4. 觸發(fā)已就緒的non-port-based輸入源。
  5. 如果基于端口的輸入源等到觸發(fā),立即處理該事件。跳到步驟9。
  6. 通知觀察者線程即將睡眠。
  7. 保持線程睡眠直到下列其中一個(gè)事件發(fā)生:
    • 基于端口的輸入源事件到達(dá)。
    • 定時(shí)器觸發(fā)。
    • Run loop超時(shí)。
    • Run loop被顯式地喚醒。
  8. 通知觀察者線程喚醒。
  9. 處理掛起事件。
    • 如果用戶定義的定時(shí)器觸發(fā),處理定時(shí)器事件并重啟run loop。跳到步驟2。
    • 如果輸入源觸發(fā),傳遞該事件。
    • 如果run loop顯式地喚醒且并不超時(shí),重啟run loop。跳到步驟2。
  10. 通知觀察者run loop已退出。

由于觀察者對(duì)定時(shí)器和輸入源的通知是在相應(yīng)事件實(shí)際發(fā)生之前傳遞的,所以通知的時(shí)間和事件實(shí)際發(fā)生的時(shí)間可能存在間隔。如果這些事件之間的時(shí)間至關(guān)重要,你可以使用sleepawake-from-sleep通知來(lái)幫助你關(guān)聯(lián)這些事件之間的時(shí)間。

由于定時(shí)器和其他周期性的事件在run loop運(yùn)行時(shí)被傳遞,從而避免了循環(huán)在傳遞事件時(shí)被中斷。這種情況典型的例子就是你在進(jìn)入run loop之后實(shí)現(xiàn)了一個(gè)鼠標(biāo)追蹤回調(diào),然后你會(huì)重復(fù)地從應(yīng)用中收到該事件。由于你的代碼是直接抓取事件,而不是讓應(yīng)用來(lái)常規(guī)地分發(fā)那些事件,活動(dòng)的定時(shí)器并不會(huì)觸發(fā)直到退出鼠標(biāo)追蹤回調(diào)之后并返回應(yīng)用控制時(shí)。

使用run loop對(duì)象可以顯式地喚醒run loop。其他事件也有可能造成run loop的喚醒。如,加入其他的non-port-based輸入源會(huì)喚醒run loop以便使得輸入源(事件)能夠被立即處理,而不是等到事件發(fā)生時(shí)才這樣做。

何時(shí)使用Run Loop?

只有當(dāng)你為應(yīng)用創(chuàng)建輔助線程時(shí)才會(huì)需要顯式地運(yùn)行一個(gè)run loop。應(yīng)用程序主線程的run loop是一個(gè)十分關(guān)鍵的結(jié)構(gòu)。因此,應(yīng)用框架會(huì)提供主線程run loop自動(dòng)開始的代碼支持。iOS中UIApplication(或者OS X中NSApplication)的run方法會(huì)啟動(dòng)主線程run loop作為常規(guī)啟動(dòng)隊(duì)列中的一步。如果你使用Xcode的項(xiàng)目模板來(lái)創(chuàng)建應(yīng)用,你不必顯式地調(diào)用這些例程。

對(duì)于輔助線程而言,需要先決定run loop是否必須,如果是這樣,才去配置并啟動(dòng)它。你不必在所有情況下為線程開啟run loop。例如,如果你使用線程做某個(gè)長(zhǎng)期且預(yù)定的工作,也許可以避免開啟run loop。Run loop在更多情況下用在與線程做額外交互的??場(chǎng)景中。當(dāng)你計(jì)劃做以下工作時(shí)你需要開啟run loop:

  • 使用基于端口或自定義輸入源來(lái)和其他線程交流時(shí)。
  • 線程上使用定時(shí)器時(shí)。
  • 在Cocoa應(yīng)用中使用任何performSelector...相關(guān)的方法時(shí)。
  • 保持線程執(zhí)行周期性的任務(wù)時(shí)。

如果你選擇使用run loop,其創(chuàng)建和配置是顯而易見的。正如所有線程編程技術(shù)一樣,你應(yīng)該有一個(gè)在合適場(chǎng)景下退出輔助線程的計(jì)劃,干凈地退出線程總是比強(qiáng)制終止它更好。

使用Run Loop對(duì)象

Run loop對(duì)象提供了向其添加輸入源、定時(shí)器、觀察者并運(yùn)行它的主接口。每個(gè)線程有單獨(dú)的一個(gè)run loop對(duì)象與其關(guān)聯(lián)。在Cocoa中,該對(duì)象作為NSRunLoop的實(shí)例。在低等級(jí)應(yīng)用中,它是一個(gè)指向CFRunLoopRef類型的指針。

獲取Run Loop對(duì)象

為獲得當(dāng)前線程的run loop,可以采取下列方式中的其中一種:

  • 在Cocoa應(yīng)用中,使用NSRunLoop的currentRunLoop類方法獲取NSRunLoop對(duì)象。
  • 使用CFRunLoopGetCurrent函數(shù)。

盡管它們不是免費(fèi)橋接的類型,當(dāng)需要時(shí)還是可以從NSRunLoop對(duì)象中獲取CFRunLoopRef類型。NSRunLoop定義了getCFRunLoop方法以返回可傳遞給Core Foundation例程的CFRunLoopRef類型。由于兩種對(duì)象都是指向相同的run loop,你可以根據(jù)需求混合調(diào)用NSRunLoop對(duì)象和CFRunLoopRef類型。

配置Run Loop

在輔助線程上運(yùn)行run loop之前,你必須為其添加至少一個(gè)輸入源或定時(shí)器。如果run loop沒有任何源來(lái)監(jiān)測(cè),它會(huì)在你試圖啟動(dòng)它時(shí)立即退出。

除了安裝源之外,你同樣可以安裝run loop觀察者并使用它們來(lái)檢測(cè)run loop執(zhí)行的不同階段。為創(chuàng)建run loop觀察者,你需要?jiǎng)?chuàng)建一個(gè)CFRunLoopObserverRef類型并使用CFRunLoopAddObserver函數(shù)將其添加到run loop中。Run loop觀察者必須使用Core Foundation創(chuàng)建,即便是Cocoa應(yīng)用中。

代碼3-1展示了run loop中綁定觀察者的主例程代碼。該示例的目的在于展示如何創(chuàng)建run loop觀察者,所以代碼簡(jiǎn)單地設(shè)置了一個(gè)監(jiān)測(cè)所有run loop事件的觀察者對(duì)象?;镜幕卣{(diào)例程(沒有顯示)被簡(jiǎn)化用日志輸出的方式代替。

代碼3-1 創(chuàng)建run loop觀察者

- (void)threadMain
{
    // 該應(yīng)用使用垃圾回收處理,所以不需要自動(dòng)釋放池
    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)建并調(diào)度定時(shí)器
    [NSTimer scheduledTimerWithTimeInterval:0.1
                                     target:self
                                   selector:@selector(doFireTimer:)
                                   userInfo:nil
                                    repeats:YES];
    
    NSInteger loopCount = 10;
    
    do
    {
        // 運(yùn)行10次run loop后觸發(fā)定時(shí)器
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
        loopCount--;
    }
    while (loopCount);
}

當(dāng)為長(zhǎng)周期線程配置run loop時(shí),最好至少添加一個(gè)輸入源以接收消息。盡管你可以只綁定一個(gè)定時(shí)器就進(jìn)入run loop,一旦定時(shí)器觸發(fā)完成它就會(huì)失效,這將導(dǎo)致該run loop退出。綁定一個(gè)重復(fù)性的定時(shí)器可以使run loop在比較長(zhǎng)的周期內(nèi)保持運(yùn)行,但會(huì)涉及到定時(shí)喚醒線程,這會(huì)有效地作為另一種形式的輪詢定時(shí)器。相比之下,輸入源會(huì)等到事件發(fā)生前,線程都會(huì)保持睡眠。

啟動(dòng)Run Loop

啟動(dòng)run loop只針對(duì)應(yīng)用中的輔助線程才是必須的操作。一個(gè)run loop必須至少有一個(gè)用于監(jiān)測(cè)的輸入源或定時(shí)器。如果沒有其中一個(gè)被綁定,run loop會(huì)立即退出。

有幾種啟動(dòng)run loop的方式,大致如下:

  • 無(wú)條件啟動(dòng)。
  • 限制時(shí)間內(nèi)啟動(dòng)。
  • 在特定模式下啟動(dòng)。

無(wú)條件地進(jìn)入run loop是最簡(jiǎn)單的選項(xiàng),但這樣做最不可取。無(wú)條件地運(yùn)行run loop會(huì)將線程置于固定的循環(huán)當(dāng)中,這會(huì)讓你失去對(duì)run loop的控制。你可以添加或移除輸入源及定時(shí)器,但停止run loop的唯一方式是終止它。同樣在自定義模式下不能運(yùn)行run loop。

除開無(wú)條件地運(yùn)行run loop,最好設(shè)置一個(gè)超時(shí)時(shí)間后運(yùn)行run loop。當(dāng)設(shè)置時(shí)間閾值后,run loop會(huì)在事件到達(dá)或時(shí)間超時(shí)后運(yùn)行。當(dāng)事件到達(dá),事件會(huì)被分發(fā)給處理回調(diào)然后run loop退出。你的代碼可以在之后重啟run loop以處理下一次事件。如果是分配時(shí)間過(guò)期,可以簡(jiǎn)單地重新啟動(dòng)run loop或重設(shè)時(shí)間來(lái)做任何需要的事務(wù)管理。

除了設(shè)置時(shí)間閾外,你同樣可以用指定的模式來(lái)運(yùn)行run loop。模式和時(shí)間閾并不互斥并可以同時(shí)用于run loop啟動(dòng)。模式會(huì)對(duì)傳遞事件的來(lái)源做出限制。

代碼3-2展示了線程主入口例程的框架版本。該示例的關(guān)鍵部分展示了run loop的基本結(jié)構(gòu)。在本質(zhì)上,你將輸入源和計(jì)時(shí)器添加到run loop,然后重復(fù)調(diào)用例程之一以啟動(dòng)run loop。每次run loop例程返回時(shí),請(qǐng)檢查是否出現(xiàn)了可能需要退出線程的任何條件。示例使用Core Foundation的run loop例程,以便它可以檢查返回結(jié)果,并確定run loop退出的原因。如果使用的是Cocoa方式且不需要檢查返回值,你也可以使用NSRunLoop類的方法以類似的方式運(yùn)行run loop。(使用NSRunLoop類的方法完成run loop調(diào)用,示例參見代碼3-14。)

代碼3-2 運(yùn)行run loop

- (void)skeletonThreadMain
{
    // 如果不使用垃圾回收處理需設(shè)置一個(gè)自動(dòng)釋放池
    BOOL done = NO;
    
    // 為run loop添加輸入源或定時(shí)器并做其他設(shè)置
    
    do
    {
        // 啟動(dòng)run loop并在每個(gè)源完成處理后返回
        SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
        
        // 如果run loop被源顯式地停止,或者不存在輸入源或定時(shí)器,則退出run loop
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
        {
            done = YES;
        }
        
        // 檢查其他的退出條件并修改done變量
    }

    while (!done);
    
    // 這里編寫清理代碼,確保釋放自動(dòng)釋放池中的對(duì)象
}

遞歸地運(yùn)行run loop是可能的情況。換句話說(shuō),你可以調(diào)用CFRunLoopRunCFRunLoopRunInMode,或任何NSRunLoop方法從輸入源或定時(shí)器的回調(diào)例程內(nèi)開始run loop。這樣做時(shí),你可以使用任何想要運(yùn)行嵌套run loop的模式,包括使用外部run loop的模式。

退出Run Loop

有兩種退出run loop的方式:

  • 配置run loop運(yùn)行超時(shí)時(shí)間閾值。
  • 通知run loop退出。

使用超時(shí)的方式當(dāng)然是首選,如果你能控制它。指定超時(shí)閾可以讓run loop結(jié)束所有的正常處理,包括在退出之前向run loop的觀察者傳遞通知。

使用CFRunLoopStop函數(shù)顯式地停止run loop產(chǎn)生的效果類似于超時(shí)。Run loop發(fā)出任何剩余的通知,然后退出。其區(qū)別在于,你可以在run loop無(wú)條件啟動(dòng)的情況下使用這種技術(shù)。

雖然移除run loop的輸入源和定時(shí)器也可能導(dǎo)致其退出,但這不是退出run loop的可靠方式。一些系統(tǒng)例程將輸入源添加到run loop來(lái)處理所需事件。因?yàn)槟愕拇a可能不知道這些輸入源,所以無(wú)法刪除它們,這會(huì)阻止run loop退出。

線程安全與Run Loop對(duì)象

線程安全取決于使用哪個(gè)接口來(lái)操作你的run loop。Core Foundation上的功能通常是線程安全的,并且可以從任何線程調(diào)用。如果你正在執(zhí)行更改run loop配置的操作,那么,它仍然是很好的做法,這樣就可以在任何可能的時(shí)候都擁有run loop的線程了。

Cocoa的NSRunLoop類并不如它在Core Foundation中的副本(譯者注:CFRunLoopRef類型)那樣線程安全。如果你正在使用NSRunLoop類修改你的run loop,你應(yīng)該只在同一個(gè)線程擁有的run loop這樣做。向?qū)儆诓煌€程的run loop添加輸入源或定時(shí)器,可能導(dǎo)致你的代碼崩潰或表現(xiàn)異常。

配置Run Loop源

以下各節(jié)將展示Cocoa和Core Foundation中如何設(shè)置輸入源的類型。

定義自定義輸入源

創(chuàng)建自定義輸入源涉及到以下定義:

  • 需要輸入源處理的信息。
  • 讓感興趣的客戶端與輸入源交互的調(diào)度器例程。
  • 處理客戶端請(qǐng)求回調(diào)的例程。
  • 注銷輸入源的取消例程。

因?yàn)槟銊?chuàng)建了自定義的輸入源來(lái)處理自定義信息,所以實(shí)際的配置是靈活的。調(diào)度程序,處理程序和取消例程幾乎是所需自定義輸入源的全部關(guān)鍵例程。然而大多數(shù)輸入源的行為發(fā)生在這些處理程序以外的例程。例如,你定義了一個(gè)用于輸入源向其他線程傳遞數(shù)據(jù)和信息交換的機(jī)制。

圖3-2展示了一個(gè)自定義的輸入源配置示例。在這個(gè)例子中,應(yīng)用程序的主線程維護(hù)著輸入源的引用,輸入源的自定義命令緩沖區(qū),以及安裝了輸入源的run loop。當(dāng)主線程有一個(gè)需交由工作線程的任務(wù)時(shí),它將向命令緩沖區(qū)發(fā)出命令以及工作線程所需要的任何信息來(lái)啟動(dòng)任務(wù)。(由于主線程和工作線程的輸入源都可以訪問(wèn)命令緩沖區(qū),所以訪問(wèn)必須是同步的。)一旦命令被發(fā)出,主線程會(huì)向輸入源發(fā)出信號(hào),并喚醒工作線程的run loop。在接收喚醒命令時(shí),run loop調(diào)用該輸入源的處理程序,輸入源會(huì)處理在命令緩沖區(qū)中找到的命令。

圖3-2 自定義輸入源的操作流程

[圖片上傳失敗...(image-675d05-1536734662071)]

以下部分闡釋了自定義輸入源的實(shí)現(xiàn),并顯示了需要實(shí)現(xiàn)的關(guān)鍵代碼。

定義輸入源

定義自定義的輸入源需要使用Core Foundation的例程,以此來(lái)對(duì)run loop源進(jìn)行配置和綁定。盡管基本的回調(diào)都是基于C語(yǔ)言的函數(shù),這并不意味著你需要為那些函數(shù)編寫Objective-C或C++的代碼包裝。

圖3-2中所示的輸入源使用Objective-C對(duì)象管理命令緩沖區(qū),并和run loop進(jìn)行調(diào)節(jié)工作。代碼3-3展示了輸入源對(duì)象的定義。RunLoopSource對(duì)象管理著命令緩沖區(qū)并使用該緩沖區(qū)來(lái)接收其他線程的消息。該段代碼同樣展示了RunLoopContext對(duì)象的定義,該對(duì)象作為傳遞RunLoopSource對(duì)象的容器對(duì)象以及run loop對(duì)應(yīng)用主線程的引用對(duì)象。

代碼3-3 自定義輸入源對(duì)象定義

@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的回調(diào)函數(shù)。這些函數(shù)在run loop源綁定到run loop后有一次初次調(diào)用,并在代碼3-4展示。因?yàn)檫@個(gè)輸入源只有一個(gè)客戶端(主線程),它會(huì)調(diào)用調(diào)度函數(shù)來(lái)向線程發(fā)送一個(gè)注冊(cè)自身應(yīng)用委托對(duì)象的消息。當(dāng)委托對(duì)象希望與輸入源通信時(shí),它會(huì)利用在RunLoopContext對(duì)象中的信息來(lái)完成。

代碼3-4 調(diào)度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];
}

最重要的回調(diào)處理例程是輸入源被通知時(shí)處理自定義數(shù)據(jù)的例程。代碼3-5展示了該例程與RunLoopSource的關(guān)聯(lián)。該函數(shù)簡(jiǎn)單地發(fā)起了一個(gè)名為sourceFired的方法的工作請(qǐng)求,sourceFired方法內(nèi)部會(huì)處理命令緩沖池中存在的任何命令。

代碼3-5 輸入源中執(zhí)行任務(wù)

void RunLoopSourcePerformRoutine (void *info)
{
    RunLoopSource*  obj = (RunLoopSource*)info;
    [obj sourceFired];
}

如果你一旦調(diào)用CFRunLoopSourceInvalidate函數(shù)將輸入源從run loop中移除,系統(tǒng)會(huì)調(diào)用輸入源的取消例程。你可以在該例程中通知客戶端輸入源將要失效并讓其移除對(duì)輸入源的引用。代碼3-6展示了使用RunLoopSource對(duì)象注冊(cè)的取消回調(diào)例程。該函數(shù)向應(yīng)用的委托對(duì)象發(fā)送了另一個(gè)RunLoopContext對(duì)象,但這次是讓請(qǐng)求對(duì)象移除對(duì)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];
}

注意:應(yīng)用委托對(duì)象的registerSource:removeSource:方法代碼在之前的代碼3-8中展示。

為Run Loop安裝輸入源

代碼3-7展示了RunLoopSource類的initaddToCurrentRunLoop方法。init方法創(chuàng)建了與run loop實(shí)際綁定的CFRunLoopSourceRef類型。它將RunLoopSource對(duì)象作為上下文信息傳遞以讓回調(diào)例程持有該對(duì)象的指針。輸入源的安裝直到工作線程調(diào)用addToCurrentRunLoop方法才發(fā)生,這時(shí)RunLoopSourceScheduleRoutine回調(diào)會(huì)被調(diào)用。一旦輸入源被添加到run loop,線程會(huì)運(yùn)行run loop并在run loop上做好準(zhǔn)備。

代碼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);
}
調(diào)節(jié)客戶端與輸入源

為了使輸入源有用,你需要對(duì)其操作并在其他線程上向其發(fā)出信號(hào)。當(dāng)無(wú)事可做時(shí)輸入源會(huì)讓關(guān)聯(lián)的線程處于睡眠狀態(tài)。這需要在應(yīng)用中的其他線程上知道該輸入源并如何和它通信。

其中的一種方式就是在輸入源初次安裝到run loop時(shí)通知客戶端輸入源將要發(fā)出注冊(cè)的請(qǐng)求。你可以為你的輸入源注冊(cè)盡可能多的客戶端,或者簡(jiǎn)單地向中心對(duì)象注冊(cè)然后向感興趣的客戶端“售出”輸入源。代碼3-8展示了應(yīng)用委托對(duì)象內(nèi)定義的注冊(cè)方法,并在RunLoopSource對(duì)象的調(diào)度函數(shù)調(diào)用。該方法從RunLoopSource對(duì)象獲取RunLoopContext對(duì)象并將其添加到它的多個(gè)源當(dāng)中。代碼也展示了將輸入源從run loop注銷的函數(shù)例程。

代碼3-8 使用應(yīng)用委托對(duì)象完成輸入源注冊(cè)與移除

- (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];
    }
}

注意:調(diào)用這些方法的回調(diào)函數(shù)代碼在之前的代碼3-4和代碼3-6中展示。

向輸入源發(fā)出信號(hào)

在向輸入源傳遞數(shù)據(jù)之后,客戶端必須向源發(fā)起信號(hào)并喚醒其run loop。向源發(fā)起信號(hào)的目的是讓run loop知道源已經(jīng)準(zhǔn)備好處理了。還有一個(gè)原因就是線程可能在發(fā)起信號(hào)時(shí)處理睡眠狀態(tài),你應(yīng)該總是顯式地喚醒run loop。如果沒能完成這樣的操作,可能會(huì)導(dǎo)致輸入源延遲處理。

代碼3-9展示了RunLoop的fireCommandsOnRunLoop方法??蛻舳嗽谳斎朐醋龊脺?zhǔn)備處理緩沖區(qū)命令時(shí)調(diào)用該方法。

代碼3-9 喚醒run loop

- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
    CFRunLoopSourceSignal(runLoopSource);
    CFRunLoopWakeUp(runloop);
}

注意:你不能通過(guò)自定義輸入源嘗試處理SIGHUP或其他類型的進(jìn)程級(jí)信號(hào)。Core Foundation中喚醒run loop的函數(shù)并不安全且不應(yīng)該在應(yīng)用的信號(hào)處理例程中使用。

配置定時(shí)源

為創(chuàng)建定時(shí)器源,所有你需要做的就是創(chuàng)建一個(gè)定時(shí)器對(duì)象并完成它在run loop上的調(diào)用。在Cocoa中,可以使用NSTimer類創(chuàng)建新的定時(shí)器對(duì)象,在Core Foundation中則使用CFRunLoopTimerRef類型。本質(zhì)上,NSTimer類是Core Foundation上提供的一種更加便利功能的擴(kuò)展,如在同一個(gè)方法上創(chuàng)建并調(diào)度定時(shí)器的能力。

在Cocoa中,你可以用以下的類方法同時(shí)完成定時(shí)器的創(chuàng)建和調(diào)度:

  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
  • scheduledTimerWithTimeInterval:invocation:repeats:

這些方法會(huì)創(chuàng)建定時(shí)器并將其添加到默認(rèn)模式下的run loop中。(NSDefaultRunLoopMode)你也可以按照需求手動(dòng)地調(diào)度定時(shí)器,這需要使用NSRunLoop的addTimer:forMode:方法來(lái)創(chuàng)建NSTimer對(duì)象并將其添加到run loop。這兩種技術(shù)都能夠完成基本的操作但給你對(duì)于定時(shí)器不同層級(jí)的控制。例如,如果創(chuàng)建定時(shí)器并手動(dòng)地添加到run loop,則可以使用除默認(rèn)模式外的其他模式。代碼3-10展示了如何使用這兩種技術(shù)來(lái)創(chuàng)建定時(shí)器。第一個(gè)定時(shí)器有1秒的創(chuàng)建延遲并在之后每隔0.1秒觸發(fā)一次。第二個(gè)定時(shí)器在初始化后的0.2秒觸發(fā)并在之后每隔0.2秒觸發(fā)一次。

代碼3-10 使用NSTimer創(chuàng)建并調(diào)度定時(shí)器

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ù)對(duì)定時(shí)器完成配置的代碼。盡管該示例在上下文并不傳遞任何用戶定義的信息,你可以使用該上下文為定時(shí)器傳遞任何自定義的數(shù)據(jù)。

代碼3-11 使用Core Foundation創(chuàng)建并調(diào)度定時(shí)器

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同時(shí)提供了用于線程或進(jìn)程間通信的基于端口的對(duì)象。下面部分將向你展示如何使用不同類型的端口來(lái)實(shí)現(xiàn)端口通信。

配置NSMachPort對(duì)象

為了使用NSMachPort對(duì)象建立本地連接,你需要?jiǎng)?chuàng)建一個(gè)端口對(duì)象并添加到主要線程的run loop中。當(dāng)啟動(dòng)輔助線程后,向該線程的入口點(diǎn)函數(shù)傳遞相同的端口對(duì)象。輔助線程就可以使用這個(gè)對(duì)象來(lái)向主要線程發(fā)送消息。

主線程代碼實(shí)現(xiàn)
代碼3-12展示了如何啟動(dòng)輔助的工作線程的主線程代碼。由于Cocoa框架實(shí)現(xiàn)了許多配置run loop端口的中介步驟,launchThread方法明顯比Core Foundation中等價(jià)的函數(shù)(代碼3-17)短;然而它們兩者行為近乎等同。唯一的區(qū)別在于該方法直接發(fā)送NSPort對(duì)象,而不是向工作線程發(fā)送本地端口的名字。

代碼3-12 主線程的啟動(dòng)方法

- (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];
    }
}

為了實(shí)現(xiàn)線程間雙邊通信的信道,你應(yīng)該想要工作線程在向主線程發(fā)送的檢入信息中包含它自己的本地端口。接收到該檢入信息能夠讓你的主線程知道輔助線程的啟動(dòng)狀況以及讓你向其發(fā)送額外的信息。

代碼3-13展示了主線程中的handlePortMessage:方法。該方法在數(shù)據(jù)到達(dá)本地端口時(shí)調(diào)用。當(dāng)檢入信息到達(dá)時(shí),該方法為輔助線程從端口信息中獲取端口并將其存儲(chǔ)起來(lái)以供使用。

代碼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.
    }
}

輔助線程代碼實(shí)現(xiàn)
對(duì)于輔助工作線程,你必須為配置該線程并使用指定端口來(lái)和主線程完成信息的回復(fù)。

代碼3-14展示了該工作線程的設(shè)置代碼。在為線程創(chuàng)建自動(dòng)釋放池后,該方法創(chuàng)建了一個(gè)工作者對(duì)象并驅(qū)動(dòng)線程的執(zhí)行。工作對(duì)象的sendCheckinMessage:方法(代碼3-15展示)創(chuàng)建了一個(gè)工作線程的本地端口并發(fā)送檢入消息給主線程。

代碼3-14 啟動(dòng)使用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];
}

當(dāng)使用NSMachPort時(shí),本地及遠(yuǎn)程的對(duì)象可以使用同樣的端口對(duì)象進(jìn)行線程間的通信。換句話講,由線程創(chuàng)建的本地端口對(duì)象可以成為其他線程的遠(yuǎn)程端口。

代碼3-15展示了輔助線程中的檢入例程。該方法設(shè)置了一個(gè)用于單邊通信的本地端口,并在之后向主線程回送了檢入信息。該方法使用從LaunchThreadWithPort:方法接收到的端口對(duì)象作為消息的目標(biāo)對(duì)象。

代碼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對(duì)象

為創(chuàng)建基于NSMessagePort對(duì)象的本地連接,你不能在線程間簡(jiǎn)單地傳遞端口對(duì)象。遠(yuǎn)程消息端口根據(jù)名稱獲得。Cocoa中需要本地端口注冊(cè)指定的名稱并將名稱傳遞給遠(yuǎn)端的線程以便其獲得用于通信的合適的端口對(duì)象。代碼3-16展示了消息端口的創(chuàng)建與注冊(cè)流程。

代碼3-16 注冊(cè)一個(gè)消息端口

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在主線程和工作線程之間設(shè)置一個(gè)雙邊通信的信道。

代碼3-17展示了主線程啟動(dòng)工作線程的調(diào)用代碼。該段代碼首先創(chuàng)建了一個(gè)CFMessagePortRef類型來(lái)監(jiān)聽工作線程上的消息。由于工作線程需要端口的名稱來(lái)建立連接,所以工作線程的入口點(diǎn)函數(shù)需要傳入字符串值。端口名稱在當(dāng)前用戶的環(huán)境下必須是唯一的;否則可能會(huì)遇到?jīng)_突。

代碼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));
}

在端口安裝完成和線程啟動(dòng)之后,主線程可以繼續(xù)它的常規(guī)執(zhí)行以等待線程的檢入。當(dāng)檢入消息到達(dá)時(shí),它會(huì)被分發(fā)到主線程的MainThreadResponseHandler函數(shù),在代碼3-18中顯示。該函數(shù)從工作線程中提取端口的名稱并創(chuàng)建好將來(lái)通信的渠道。

代碼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;
}

完成主線程的配置后,唯一剩下的事情就是對(duì)新創(chuàng)建工作線程創(chuàng)建它自己的端口并檢入。代碼3-19展示了工作線程的入口函數(shù)。該函數(shù)從主線程中提取到端口名并使用它來(lái)創(chuàng)建回連主線程的端口。該函數(shù)然后創(chuàng)建了自己的安裝在run loop上的一個(gè)本地端口,并向主線程發(fā)送一條包含本地端口名的檢入消息。

代碼3-19 創(chuàng)建線程結(jié)構(gòu)

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();
}

一旦它進(jìn)入run loop,所有將來(lái)發(fā)向該線程端口的事件都將由ProcessClientRequest函數(shù)處理。該函數(shù)的實(shí)現(xiàn)依賴于線程的具體工作且不在這里做展示。

線程同步

在應(yīng)用程序中多線程的存在下,多線程的執(zhí)行產(chǎn)生了安全訪問(wèn)資源的潛在問(wèn)題。兩個(gè)線程可能不經(jīng)意間就會(huì)修改相同的資源。例如,一個(gè)線程可能覆蓋另一個(gè)線程的更改,或者將應(yīng)用程序置于一個(gè)未知的和潛在的無(wú)效狀態(tài)。如果你比較幸運(yùn),損壞的資源可能會(huì)導(dǎo)致明顯的性能問(wèn)題或崩潰,這比較容易跟蹤和修復(fù)。但是如果你比較不幸,這導(dǎo)致的微小錯(cuò)誤并不能即時(shí)被察覺或錯(cuò)誤需要代碼修改多處才能解決。

說(shuō)到線程安全,一個(gè)好的設(shè)計(jì)才是最好的保護(hù)方式。避免資源共享和減少線程之間的交互,使這些線程間不太那么容易相互干擾。然而一個(gè)完全避免干擾的設(shè)計(jì)是不可能的。在線程必須進(jìn)行交互的前提下,你需要使用同步工具來(lái)保證它們之間的交互是安全的。

OS X和iOS提供眾多的同步工具供你使用,工具從提供互斥訪問(wèn)到事件隊(duì)列。下面的章節(jié)將介紹這些工具以及如何在代碼中使用它們,以解決程序資源安全訪問(wèn)的問(wèn)題。

同步工具

為了防止線程超出預(yù)期的修改數(shù)據(jù),可以對(duì)應(yīng)用的設(shè)計(jì)做出修改或者使用同步工具避免同步問(wèn)題。雖然完全避免同步問(wèn)題是可取的,但它并不總是可能的。下面的章節(jié)描述了可供你使用的同步工具的基本類別。

原子操作

原子操作是作用于簡(jiǎn)單數(shù)據(jù)類型上的簡(jiǎn)單同步形式。其優(yōu)勢(shì)在于它不阻塞相互競(jìng)爭(zhēng)的線程。對(duì)于簡(jiǎn)單的操作,譬如增加計(jì)數(shù)器變量,它比鎖的性能更加優(yōu)越。

OS X和iOS包括了多個(gè)用于32位及64位基本數(shù)學(xué)運(yùn)算和邏輯運(yùn)算的操作。在這些操作中屬于原子性的有compare-and-swap、test-and-set,以及test-and-clear。請(qǐng)查看/usr/include/libkern/OSAtomic.h頭文件或查看atomic的man幫助頁(yè)獲取原子操作支持的列表。

內(nèi)存屏障與Volatile變量

為達(dá)到最優(yōu)性能,編譯器時(shí)常記錄匯編級(jí)別的指令以使處理器能盡可能滿載地處理流水線指令。作為該優(yōu)化的一部分,編譯器會(huì)記錄那些不會(huì)造成錯(cuò)誤數(shù)據(jù)的主內(nèi)存訪問(wèn)指令。不幸的是,編譯器不能總是檢測(cè)到所有的內(nèi)存獨(dú)立操作。如果看似獨(dú)立的變量在實(shí)際上卻相互影響,編譯器就會(huì)以錯(cuò)誤的順序?qū)ζ涓拢a(chǎn)生潛在的錯(cuò)誤結(jié)果。

內(nèi)存屏障作為一種非阻塞式的同步工具,其用于確保內(nèi)存操作處于正確順序。內(nèi)存屏障的角色近乎于“柵欄”,它會(huì)強(qiáng)制處理器完成任何位于它之前載入和存儲(chǔ)的操作,而將其后的操作阻擋在外。內(nèi)存屏障典型地應(yīng)用于確保線程上內(nèi)存操作(操作結(jié)果對(duì)其他線程可見)總是按照預(yù)定順序執(zhí)行。如果在剛才的情形下缺少了內(nèi)存屏障,這將導(dǎo)致其他線程得到不可預(yù)計(jì)的結(jié)果。(譯者注:這里說(shuō)的情形是讀寫者問(wèn)題)為使用內(nèi)存屏障,你可以在代碼合適的位置簡(jiǎn)單地調(diào)用OSMemoryBarrier函數(shù)。

Volatile變量應(yīng)用于對(duì)于單獨(dú)變量的內(nèi)存限制。編譯器優(yōu)化代碼時(shí)常通過(guò)將變量的值載入寄存器中。對(duì)于本地變量,這通常沒有什么問(wèn)題。如果一個(gè)變量對(duì)于其他線程可見,那么這樣的優(yōu)化也許會(huì)對(duì)其他線程對(duì)于該變量值修改的觀察造成影響。對(duì)變量使用volatile關(guān)鍵字可以強(qiáng)制編譯器每次使用變量時(shí)從內(nèi)存中載入。你可以在變量可能任何時(shí)候被外部修改且編譯器不能檢測(cè)到時(shí)考慮聲明變量為volatile。

鎖作為最為常用的同步工具。你可以使用鎖來(lái)保護(hù)代碼的臨界區(qū),臨界區(qū)僅能在同一時(shí)刻被單個(gè)線程訪問(wèn)。例如,臨界區(qū)在同一時(shí)刻可能會(huì)操作一個(gè)特殊的數(shù)據(jù)結(jié)構(gòu)或者占用只支持一個(gè)用戶使用的資源。在該區(qū)域加入鎖,你就可以排除其他線程造成的修改和對(duì)代碼正確性造成影響。

表4-1列舉了程序猿常用的鎖方式。OS X和iOS提供了其中大多數(shù)鎖的實(shí)現(xiàn),但并不是全部。對(duì)于不支持的鎖類型,描述字段會(huì)解釋其未在該平臺(tái)直接實(shí)現(xiàn)的原因。

表4-1 鎖類型

鎖類型 描述
互斥鎖(Mutex) 互斥排他(或互斥)鎖作為資源的保護(hù)屏障。互斥鎖是一種信號(hào)量類型并在同一時(shí)刻只為一個(gè)線程授權(quán)訪問(wèn)資源。如果使用了互斥鎖然而其他線程試圖請(qǐng)求它,那個(gè)線程會(huì)阻塞直到互斥鎖被它原先的持有者釋放。如果多個(gè)線程競(jìng)爭(zhēng)同一個(gè)互斥鎖,只有其中一個(gè)在同一時(shí)刻才有權(quán)擁有它。
遞歸鎖(Recursive lock) 遞歸鎖是互斥鎖的一個(gè)變種。遞歸鎖允許同一個(gè)線程在其釋放前多次請(qǐng)求它。其他的線程則會(huì)阻塞直到鎖的持有者以和請(qǐng)求次數(shù)相同的釋放次數(shù)釋放該鎖。遞歸鎖主要用于遞歸操作,但也可以用于多個(gè)方法間獨(dú)立地請(qǐng)求該鎖。
讀寫鎖(Read-write lock) 讀寫鎖可以被視作共享的排他鎖。該類型的鎖典型地應(yīng)用于大批量的操作以及被保護(hù)數(shù)據(jù)讀操作頻繁而寫操作稀少的場(chǎng)景中。正常操作情況下,大量的讀者可以同時(shí)訪問(wèn)數(shù)據(jù)。然而當(dāng)線程想寫數(shù)據(jù)時(shí),它直到讀者釋放鎖前都會(huì)阻塞,只有請(qǐng)求到該鎖時(shí)才能進(jìn)行數(shù)據(jù)更新的操作。當(dāng)寫入線程等待鎖時(shí),新的讀者線程會(huì)阻塞直到寫線程完成。系統(tǒng)僅支持POSIX線程使用讀寫鎖。
分布式鎖(Distributed lock) 分布式鎖提供進(jìn)程級(jí)別的互斥訪問(wèn)。不像真正的互斥。分布式鎖不阻塞進(jìn)程的運(yùn)行。它僅報(bào)告鎖很忙并讓進(jìn)程決定如何執(zhí)行。
自旋鎖(Spin lock) 自旋鎖在條件滿足前會(huì)不斷地輪詢獲得鎖。自旋鎖常用于預(yù)計(jì)等待鎖的時(shí)間短的多處理器系統(tǒng)中。在這樣的前提下,輪詢得到鎖通常比阻塞線程更加高效,因?yàn)楹笳呱婕暗骄€程狀態(tài)的切換和線程數(shù)據(jù)的更新。由于其輪詢的特性,系統(tǒng)不直接提供自旋鎖的實(shí)現(xiàn),但你可以在特定環(huán)境中簡(jiǎn)單地實(shí)現(xiàn)。(譯者注:已經(jīng)不建議最新系統(tǒng)中使用OSSpinLock,了解更多信息請(qǐng)移步這里
雙重檢查鎖(Double-checked lock) 雙重檢查鎖為減少持有鎖帶來(lái)的資源消耗,它總是在持有鎖前先測(cè)試鎖的臨界條件。由于雙重檢查鎖有潛在的安全問(wèn)題,系統(tǒng)并不顯式地提供支持且不鼓勵(lì)使用它。

注意:大多數(shù)類型的鎖也包含了內(nèi)存屏障區(qū),以確保進(jìn)入臨界區(qū)前完成先前需要完成的指令。

條件量

條件量作為另一種信號(hào)量類型以允許線程在某種條件滿足時(shí)相互通知。條件量常用于預(yù)示資源的可用性或者確保任務(wù)以特定順序執(zhí)行。當(dāng)線程在測(cè)試條件時(shí),它會(huì)一直阻塞直到條件為真。同樣地,直到其他線程顯式地修改條件前也會(huì)保持阻塞狀態(tài)。條件量與互斥鎖的區(qū)別在于多個(gè)線程能夠同時(shí)訪問(wèn)鎖。條件量更像一個(gè)守門員一樣,它根據(jù)某些特定的條件讓不同的線程通過(guò)這道“門”。

一種可能使用條件量的情形是管理一系列掛起的事件。事件隊(duì)列需要使用一個(gè)條件變量來(lái)通知隊(duì)列中有等待事件的線程。如果事件到達(dá),隊(duì)列會(huì)正確地喚起條件量。如果線程仍在等待,它會(huì)因?yàn)樾枰獙⑹录湃腙?duì)列并處理而被喚醒。如果兩個(gè)事件幾乎同時(shí)進(jìn)入隊(duì)列,隊(duì)列會(huì)通知條件量?jī)纱尾酒饍蓚€(gè)線程。

系統(tǒng)以幾種不同的技術(shù)為條件量提供支持。條件量的實(shí)現(xiàn)需要仔細(xì)的代碼編寫,所以你需要在編寫自己的代碼前查看后面的示例代碼。

Perform Selector

Cocoa應(yīng)用有以同步形式向單個(gè)線程傳遞信息的便利方式。NSObject類聲明了在應(yīng)用活動(dòng)線程上執(zhí)行選擇器的一些方法。這些方法使你的線程異步地傳遞信息并保證在目標(biāo)線程上同步地執(zhí)行。例如,你可能使用這種方式向應(yīng)用的主線程或者指定線程傳遞分布式運(yùn)算的結(jié)果。每次執(zhí)行選擇器的請(qǐng)求都被存儲(chǔ)在線程run loop的事件隊(duì)列中,并且請(qǐng)求會(huì)按照他們被收到時(shí)的順序線性地執(zhí)行。

同步消耗與性能

同步助你確保代碼的正確性,但那樣做確實(shí)損耗不少性能。即便在無(wú)沖突的情況下,同步工具都不是首選。鎖和原子操作通常涉及到內(nèi)存屏障和內(nèi)核級(jí)同步過(guò)程的使用來(lái)確保代碼受到正確保護(hù)。如果出現(xiàn)了鎖的沖突,你的線程會(huì)阻塞并經(jīng)歷更嚴(yán)重的延遲。

表4-2列舉了使用互斥和原子操作的近似消耗。這些數(shù)據(jù)作為從幾千份樣本中采集的平均值。由于考慮了線程的創(chuàng)建時(shí)間,互斥鎖獲取時(shí)間會(huì)根據(jù)處理器的負(fù)載、計(jì)算機(jī)速度、系統(tǒng)內(nèi)存和程序內(nèi)存而千差萬(wàn)別。

表4-2 互斥和原子操作資源消耗

指標(biāo) 近似消耗 注釋
互斥鎖獲取時(shí)間 接近0.2微秒 這是在無(wú)沖突的情況下鎖的獲取時(shí)間。如果鎖被另一個(gè)線程持有,獲取時(shí)間還會(huì)更多。該數(shù)據(jù)由平臺(tái)上(基于Intel處理器的iMac/2GHz雙核處理器/1G內(nèi)存/OS X 10.5系統(tǒng))分析平均值和中位值測(cè)定的數(shù)據(jù)決定。
原子性的compare-and-swap 接近0.05微秒 這是在無(wú)沖突的情況下執(zhí)行compare-and-swap操作的花費(fèi)時(shí)間。該數(shù)據(jù)由平臺(tái)上(基于Intel處理器的iMac/2GHz雙核處理器/1G內(nèi)存/OS X 10.5系統(tǒng))分析平均值和中位值測(cè)定的數(shù)據(jù)決定。

當(dāng)你設(shè)計(jì)并發(fā)任務(wù)時(shí),正確性才通常是最為重要的考慮因素,但是你也應(yīng)該考慮性能因素。代碼在多線程的方式下正確執(zhí)行,但是卻比單線程執(zhí)行更慢,這就難說(shuō)是一種改進(jìn)了。

如果你正在對(duì)一個(gè)現(xiàn)有的單線程應(yīng)用程序進(jìn)行改造,你應(yīng)該經(jīng)常設(shè)置一系列關(guān)鍵任務(wù)的性能基線測(cè)量值。在添加附加的線程后,你應(yīng)該對(duì)于相同的任務(wù)收集新的數(shù)據(jù)并比較單線程和多線程的性能差異。如果在調(diào)整代碼后,線程并沒有提高性能,你可能希望重新考慮你的具體實(shí)現(xiàn)或使用線程的方式問(wèn)題。

更多關(guān)于性能和數(shù)據(jù)收集工具的信息,請(qǐng)看[譯]性能概述

線程安全與信號(hào)量

當(dāng)遇到多線程應(yīng)用,沒有什么比處理信號(hào)量更讓人恐懼和困惑的了。信號(hào)量是一種低等級(jí)的BSD機(jī)制以便能用于傳輸給信息進(jìn)程或以某種方式處理信息。一些程序使用信號(hào)量來(lái)檢測(cè)特定事件,比如子進(jìn)程的死亡。系統(tǒng)會(huì)使用信號(hào)量來(lái)終止失去控制的進(jìn)程以及傳遞其他類型的信息。

信號(hào)量的問(wèn)題并不在它們做的是什么,而是應(yīng)用有多個(gè)線程時(shí)它們的行為。在單線程應(yīng)用中,所有的信號(hào)量處理回調(diào)都在主線程上。在多線程應(yīng)用中,不依賴于任何硬件錯(cuò)誤的(如非法指令)的信號(hào)被傳遞到任何一個(gè)線程的合適時(shí)機(jī)下運(yùn)行。如果多個(gè)線程同時(shí)運(yùn)行,信號(hào)量會(huì)被傳遞到系統(tǒng)所選的任何一個(gè)線程。換句話說(shuō),信號(hào)量可以被傳遞到應(yīng)用中的任何線程。

在應(yīng)用程序中實(shí)現(xiàn)信號(hào)處理回調(diào)的首要規(guī)則是避免線程處理該信號(hào)的假設(shè)。如果某個(gè)特定的線程需要處理給定的信號(hào),則需要編寫在信號(hào)到達(dá)時(shí)通知該線程的一些方法。你不能只假設(shè)線程中信號(hào)處理回調(diào)的安裝會(huì)讓該信號(hào)被傳遞到這個(gè)線程。

獲取更多關(guān)于信號(hào)量和信號(hào)量回調(diào)安裝的信息,請(qǐng)查看signal和sigaction的man幫助頁(yè)面。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容