runlooop

Run loops是線程相關的的基礎框架的一部分。一個run loop就是一個事件處理的循環,用來不停的調度工作以及處理輸入事件。使用run loop的目的是讓你的線程在有工作的時候忙於工作,而沒工作的時候處於休眠狀態。

Run loop的管理並不完全自動的。你仍然需要設計你的線程代碼在合適的時候啓動run loop並正確響應輸入事件。Cocoa和Core Fundation都提供了run loop objects來幫助配置和管理你線程的run loop。你的應用程序不需要顯式的創建這些對象(run loop objects);每個線程,包括程序的主線程都有與之對應的run loop object。只有輔助線程才需要顯式的運行它的run loop。在Carbon和Cocoa程序中,主線程會自動創建並運行它run loop,作爲一般應用程序啓動過程的一部分。

以下各部分提供更多關於run loops以及如何爲你的應用程序配置它們。關於run loop object的額外信息,參閱NSRunLoop Class Reference和CFRunLoop Reference文檔。

1.1        Run Loop剖析

Run loop本身聽起來就和它的名字很像。它是一個循環,你的線程進入並使用它來運行響應輸入事件的事件處理程序。你的代碼要提供實現循環部分的控制語句,換言之就是要有while或for循環語句來驅動run loop。在你的循環中,使用run loop object來運行事件處理代碼,它響應接收到的事件並啓動已經安裝的處理程序。

Run loop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時源(timer source)。輸入源傳遞異步事件,通常消息來自於其他線程或程序。定時源則傳遞同步事件,發生在特定時間或者重複的時間間隔。兩種源都使用程序的某一特定的處理例程來處理到達的事件。

圖3-1顯示了run loop的概念結構以及各種源。輸入源傳遞異步消息給相應的處理例程,並調用runUntilDate:方法來退出(在線程裏面相關的NSRunLoop對象調用)。定時源則直接傳遞消息給處理例程,但並不會退出run loop。



除了處理輸入源,run loops也會生成關於run loop行爲的通知(notifications)。註冊的run loop觀察者(run-loop Observers)可以收到這些通知,並在線程上面使用它們來做額外的處理。你可以使用Core Foundation在你的線程註冊run-loop觀察者。

下面部分介紹更多關於run loop的構成,以及其運行的模式。同時也提及在處理事件中不同時間生成的通知。

1.1.1    Run Loop 模式

Run loop模式是所有要監視的輸入源和定時源以及要通知的run loop註冊觀察者的集合。每次運行你的run loop,你都要指定(無論顯示還是隱式)其運行個模式。在run loop運行過程中,只有和模式相關的源纔會被監視並允許他們傳遞事件消息。(類似的,只有和模式相關的觀察者會通知run loop的進程)。和其他模式關聯的源只有在run loop運行在其模式下才會運行,否則處於暫停狀態。

通常在你的代碼中,你可以通過指定名字來標識模式。Cocoa和Core foundation定義了一個默認的和一些常用的模式,在你的代碼中都是用字符串來標識這些模式。當然你也可以給模式名稱指定一個字符串來自定義模式。雖然你可以給模式指定任意名字,但是模式的內容則不能是任意的。你必須添加一個或多個輸入源,定時源或者run loop的觀察者到你新建的模式中讓他們有價值。

通過指定模式可以使得run loop在某一階段過濾來源於源的事件。大多數時候,run loop都是運行在系統定義的默認模式上。但是模態面板(modal panel)可以運行在 “modal”模式下。在這種模式下,只有和模式面板相關的源纔可以傳遞消息給線程。對於輔助線程,你可以使用自定義模式在一個時間週期操作上屏蔽優先級低的源傳遞消息。

注意:模式區分基於事件的源而非事件的種類。例如,你不可以使用模式只選擇處理鼠標按下或者鍵盤事件。你可以使用模式監聽端口,暫停定時器或者改變其他源或者當前模式下處於監聽狀態run loop觀察者。

表1-3列出了Cocoa和Core Foundation定義的標準模式,並且介紹何時使用他們。名稱那列列出了你用來在你代碼中指定模式實際的常量。

Table 3-1  Predefined run loop modes

Mode

Name

Description

Default

NSDefaultRunLoopMode(Cocoa)

kCFRunLoopDefaultMode (Core Foundation)

The default mode is the one used for most operations. Most of the time, you should use this mode to start your run loop and configure your input sources.

Connection

NSConnectionReplyMode(Cocoa)

Cocoa uses this mode in conjunction with NSConnection objects to monitor replies. You should rarely need to use this mode yourself.

Modal

NSModalPanelRunLoopMode(Cocoa)

Cocoa uses this mode to identify events intended for modal panels.

Event tracking

NSEventTrackingRunLoopMode(Cocoa)

Cocoa uses this mode to restrict incoming events during mouse-dragging loops and other sorts of user interface tracking loops.

Common modes

NSRunLoopCommonModes(Cocoa)

kCFRunLoopCommonModes (Core Foundation)

This is a configurable group of commonly used modes. Associating an input source with this mode also associates it with each of the modes in the group. For Cocoa applications, this set includes the default, modal, and event tracking modes by default. Core Foundation includes just the default mode initially. You can add custom modes to the set using theCFRunLoopAddCommonMode function.


1.1.2    輸入源

輸入源異步的發送消息給你的線程。事件來源取決於輸入源的種類:基於端口的輸入源自定義輸入源。基於端口的輸入源監聽程序相應的端口。自定義輸入源則監聽自定義的事件源。至於run loop,它不關心輸入源的是基於端口的輸入源還是自定義的輸入源。系統會實現兩種輸入源供你使用。兩類輸入源的區別在於如何顯示:基於端口的輸入源由內核自動發送,而自定義的則需要人工從其他線程發送。

當你創建輸入源,你需要將其分配給run loop中的一個或多個模式。模式只會在特定事件影響監聽的源。大多數情況下,run loop運行在默認模式下,但是你也可以使其運行在自定義模式。若某一源在當前模式下不被監聽,那麼任何其生成的消息只在run loop運行在其關聯的模式下才會被傳遞。

 

基於端口的輸入源

Cocoa和Core Foundation內置支持使用端口相關的對象和函數來創建的基於端口的源。例如,在Cocoa裏面你從來不需要直接創建輸入源。你只要簡單的創建端口對象,並使用NSPort的方法把該端口添加到run loop。端口對象會自己處理創建和配置輸入源。

在Core Foundation,你必須人工創建端口和它的run loop源.在兩種情況下,你都可以使用端口相關的函數(CFMachPortRef,CFMessagePortRef,CFSocketRef)來創建合適的對象。

更多例子關於如何設置和配置一個自定義端口源,參閱“配置一個基於端口的輸入源”部分。

 

自定義輸入源

爲了創建自定義輸入源,必須使用Core Foundation裏面的CFRunLoopSourceRef類型相關的函數來創建。你可以使用回調函數來配置自定義輸入源。Core Fundation會在配置源的不同地方調用回調函數,處理輸入事件,在源從run loop移除的時候清理它。

除了定義在事件到達時自定義輸入源的行爲,你也必須定義消息傳遞機制。源的這部分運行在單獨的線程裏面,並負責在數據等待處理的時候傳遞數據給源並通知它處理數據。消息傳遞機制的定義取決於你,但最好不要過於複雜。

關於創建自定義輸入源的例子,參閱“定義一個自定義輸入源”。關於自定義輸入源的信息,參閱CFRunLoopSource Reference。

 

Cocoa 執行 Selector 的源

除了基於端口的源,Cocoa定義了自定義輸入源,允許你在任何線程執行selector。和基於端口的源一樣,執行selector請求會在目標線程上序列化,減緩許多在線程上允許多個方法容易引起的同步問題。不像基於端口的源,一個selector執行完後會自動從run loop裏面移除。

注意:在Mac OS X v10.5之前,執行selector多半可能是給主線程發送消息,但是在Mac OS X v10.5及其之後和在iOS裏面,你可以使用它們給任何線程發送消息。

當在其他線程上面執行selector時,目標線程須有一個活動的run loop。對於你創建的線程,這意味着線程在你顯式的啓動run loop之前處於等待狀態。由於主線程自己啓動它的run loop,那麼在程序通過委託調用applicationDidFinishlaunching:的時候你會遇到線程調用的問題。因爲Run loop通過每次循環來處理所有隊列的selector的調用,而不是通過loop的迭代來處理selector。

表3-2列出了NSObject中可在其它線程執行的selector。由於這些方法時定義在NSObject中,你可以在任何可以訪問Objective-C對象的線程裏面使用它們,包括POSIX的所有線程。這些方法實際上並沒有創建新的線程執行selector。

Table 3-2  Performing selectors on other threads

Methods

Description

performSelectorOnMainThread:withObject:waitUntilDone:

performSelectorOnMainThread:withObject:waitUntilDone:modes:

Performs the specified selector on the application’s main thread during that thread’s next run loop cycle. These methods give you the option of blocking the current thread until the selector is performed.

performSelector:onThread:withObject:waitUntilDone:

performSelector:onThread:withObject:waitUntilDone:modes:

Performs the specified selector on any thread for which you have an NSThreadobject. These methods give you the option of blocking the current thread until the selector is performed.

performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:inModes:

Performs the specified selector on the current thread during the next run loop cycle and after an optional delay period. Because it waits until the next run loop cycle to perform the selector, these methods provide an automatic mini delay from the currently executing code. Multiple queued selectors are performed one after another in the order they were queued.

cancelPreviousPerformRequestsWithTarget:

cancelPreviousPerformRequestsWithTarget:selector:object:

Lets you cancel a message sent to the current thread using theperformSelector:withObject:afterDelay:orperformSelector:withObject:afterDelay:inModes:method.

 

關於更多介紹這些方法的信息,參閱NSObject Class Reference。

 

定時源

定時源在預設的時間點同步方式傳遞消息。定時器是線程通知自己做某事的一種方法。例如,搜索控件可以使用定時器,當用戶連續輸入的時間超過一定時間時,就開始一次搜索。這樣使用延遲時間,就可以讓用戶在搜索前有足夠的時間來輸入想要搜索的關鍵字。

經管定時器可以產生基於時間的通知,但它並不是實時機制。和輸入源一樣,定時器也和你的run loop的特定模式相關。如果定時器所在的模式當前未被run loop監視,那麼定時器將不會開始直到run loop運行在相應的模式下。類似的,如果定時器在run loop處理某一事件期間開始,定時器會一直等待直到下次run loop開始相應的處理程序。如果run loop不再運行,那定時器也將永遠不啓動。

你可以配置定時器工作僅一次還是重複工作。重複工作定時器會基於安排好的時間而非實際時間調度它自己運行。舉個例子,如果定時器被設定在某一特定時間開始並5秒重複一次,那麼定時器會在那個特定時間後5秒啓動,即使在那個特定的觸發時間延遲了。如果定時器被延遲以至於它錯過了一個或多個觸發時間,那麼定時器會在下一個最近的觸發事件啓動,而後面會按照觸發間隔正常執行。

關於更多配置定時源的信息,參閱“配置定時源”部分。關於引用信息,查看NSTimer Class Reference或CFRunLoopTimer Reference。

 

Run Loop觀察者

源是合適的同步或異步事件發生時觸發,而run loop觀察者則是在run loop本身運行的特定時候觸發。你可以使用run loop觀察者來爲處理某一特定事件或是進入休眠的線程做準備。你可以將run loop觀察者和以下事件關聯:

  • Run loop入口
  • Run loop何時處理一個定時器
  • Run loop何時處理一個輸入源
  • Run loop何時進入睡眠狀態
  • Run loop何時被喚醒,但在喚醒之前要處理的事件
  • Run loop終止

你可以給run loop觀察者添加到Cocoa和Carbon程序裏面,但是如果你要定義觀察者並把它添加到run loop的話,那就只能使用Core Fundation了。爲了創建一個run loop觀察者,你可以創建一個CFRunLoopObserverRef類型的實例。它會追蹤你自定義的回調函數以及其它你感興趣的活動。

和定時器類似,run loop觀察者可以只用一次或循環使用。若只用一次,那麼在它啓動後,會把它自己從run loop裏面移除,而循環的觀察者則不會。你在創建run loop觀察者的時候需要指定它是運行一次還是多次。

關於如何創建一個run loop觀察者的實例,參閱“配置run loop”部分。關於更多的相關信息,參閱CFRunLoopObserver Reference。


Run Loop的事件隊列

每次運行run loop,你線程的run loop對會自動處理之前未處理的消息,並通知相關的觀察者。具體的順序如下:

  1. 通知觀察者run loop已經啓動
  2. 通知觀察者任何即將要開始的定時器
  3. 通知觀察者任何即將啓動的非基於端口的源
  4. 啓動任何準備好的非基於端口的源
  5. 如果基於端口的源準備好並處於等待狀態,立即啓動;並進入步驟9。
  6. 通知觀察者線程進入休眠
  7. 將線程置於休眠直到任一下面的事件發生:
    • 某一事件到達基於端口的源
    • 定時器啓動
    • Run loop設置的時間已經超時
    • run loop被顯式喚醒
  8. 通知觀察者線程將被喚醒。
  9. 處理未處理的事件
    • 如果用戶定義的定時器啓動,處理定時器事件並重啓run loop。進入步驟2
    • 如果輸入源啓動,傳遞相應的消息
    • 如果run loop被顯式喚醒而且時間還沒超時,重啓run loop。進入步驟2
  10. 通知觀察者run loop結束。

因爲定時器和輸入源的觀察者是在相應的事件發生之前傳遞消息,所以通知的時間和實際事件發生的時間之間可能存在誤差。如果需要精確時間控制,你可以使用休眠和喚醒通知來幫助你校對實際發生事件的時間。

因爲當你運行run loop時定時器和其它週期性事件經常需要被傳遞,撤銷run loop也會終止消息傳遞。典型的例子就是鼠標路徑追蹤。因爲你的代碼直接獲取到消息而不是經由程序傳遞,因此活躍的定時器不會開始直到鼠標追蹤結束並將控制權交給程序。

Run loop可以由run loop對象顯式喚醒。其它消息也可以喚醒run loop。例如,添加新的非基於端口的源會喚醒run loop從而可以立即處理輸入源而不需要等待其他事件發生後再處理。

1.2        何時使用Run Loop

僅當在爲你的程序創建輔助線程的時候,你才需要顯式運行一個run loop。Run loop是程序主線程基礎設施的關鍵部分。所以,Cocoa和Carbon程序提供了代碼運行主程序的循環並自動啓動run loop。IOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作爲程序啓動步驟的一部分,它在程序正常啓動的時候就會啓動程序的主循環。類似的,RunApplicationEventLoop函數爲Carbon程序啓動主循環。如果你使用xcode提供的模板創建你的程序,那你永遠不需要自己去顯式的調用這些例程。

對於輔助線程,你需要判斷一個run loop是否是必須的。如果是必須的,那麼你要自己配置並啓動它。你不需要在任何情況下都去啓動一個線程的run loop。比如,你使用線程來處理一個預先定義的長時間運行的任務時,你應該避免啓動run loop。Run loop在你要和線程有更多的交互時才需要,比如以下情況:

  1. 使用端口或自定義輸入源來和其他線程通信
  2. 使用線程的定時器
  3. Cocoa中使用任何performSelector…的方法
  4. 使線程週期性工作

如果你決定在程序中使用run loop,那麼它的配置和啓動都很簡單。和所有線程編程一樣,你需要計劃好在輔助線程退出線程的情形。讓線程自然退出往往比強制關閉它更好。關於更多介紹如何配置和退出一個run loop,參閱”使用Run Loop對象”的介紹。

1.3        使用Run Loop對象

Run loop對象提供了添加輸入源,定時器和run loop的觀察者以及啓動run loop的接口。每個線程都有唯一的與之關聯的run loop對象。在Cocoa中,該對象是NSRunLoop類的一個實例;而在Carbon或BSD程序中則是一個指向CFRunLoopRef類型的指針。

1.3.1    獲得Run Loop對象

爲了獲得當前線程的run loop,你可以採用以下任一方式:

  • 在Cocoa程序中,使用NSRunLoop的currentRunLoop類方法來檢索一個NSRunLoop對象。
  • 使用CFRunLoopGetCurrent函數。

雖然它們並不是完全相同的類型,但是你可以在需要的時候從NSRunLoop對象中獲取CFRunLoopRef類型。NSRunLoop類定義了一個getCFRunLoop方法,該方法返回一個可以傳遞給Core Foundation例程的CFRunLoopRef類型。因爲兩者都指向同一個run loop,你可以在需要的時候混合使用NSRunLoop對象和CFRunLoopRef不透明類型。

1.3.2    配置Run Loop

在你在輔助線程運行run loop之前,你必須至少添加一輸入源或定時器給它。如果run loop沒有任何源需要監視的話,它會在你啓動之際立馬退出。關於如何添加源到run loop裏面的例子,參閱”配置Run Loop源”。

除了安裝源,你也可以添加run loop觀察者來監視run loop的不同執行階段情況。爲了給run loop添加一個觀察者,你可以創建CFRunLoopObserverRef不透明類型,並使用CFRunLoopAddObserver將它添加到你的run loop。Run loop觀察者必須由Core foundation函數創建,即使是Cocoa程序。

列表3-1顯示了附加一個run loop的觀察者到它的run loop的線程主體例程。該例子的主要目的是顯示如何創建一個run loop觀察者,所以該代碼只是簡單的設置一個觀察者來監視run loop的所有活動。基礎處理程序(沒有顯示)只是簡單的打印出run loop活動處理定時器請求的日誌信息。

Listing 3-1  Creating a run loop observer

 

 噹噹前長時間運行的線程配置run loop的時候,最好添加至少一個輸入源到run loop以接收消息。雖然你可以使用附屬的定時器來進入run loop,但是一旦定時器觸發後,它通常就變爲無效了,這會導致run loop退出。雖然附加一個循環的定時器可以讓run loop運行一個相對較長的週期,但是這也會導致週期性的喚醒線程,這實際上是輪詢(polling)的另一種形式而已。與之相反,輸入源會一直等待某事件發生,在事情導致前它讓線程處於休眠狀態。

1.3.3    啓動Run Loop

啓動run loop只對程序的輔助線程有意義。一個run loop通常必須包含一個輸入源或定時器來監聽事件。如果一個都沒有,run loop啓動後立即退出。

有幾種方式可以啓動run loop,包括以下這些:

l  無條件的

l  設置超時時間

l  特定的模式

無條件的進入run loop是最簡單的方法,但也最不推薦使用的。因爲這樣會使你的線程處在一個永久的循環中,這會讓你對run loop本身的控制很少。你可以添加或刪除輸入源和定時器,但是退出run loop的唯一方法是殺死它。沒有任何辦法可以讓這run loop運行在自定義模式下。

替代無條件進入run loop更好的辦法是用預設超時時間來運行run loop,這樣run loop運作直到某一事件到達或者規定的時間已經到期。如果是事件到達,消息會被傳遞給相應的處理程序來處理,然後run loop退出。你可以重新啓動run loop來等待下一事件。如果是規定時間到期了,你只需簡單的重啓run loop或使用此段時間來做任何的其他工作。

除了超時機制,你也可以使用特定的模式來運行你的run loop。模式和超時不是互斥的,他們可以在啓動run loop的時候同時使用。模式限制了可以傳遞事件給run loop的輸入源的類型,這在”Run Loop模式”部分介紹。

列表3-2描述了線程的主要例程的架構。本示例的關鍵是說明了run loop的基本結構。本質上講你添加自己的輸入源或定時器到run loop裏面,然後重複的調用一個程序來啓動run loop。每次run loop返回的時候,你需要檢查是否有使線程退出的條件成立。示例中使用了Core Foundation的run loop例程,以便可以檢查返回結果從而確定run loop爲何退出。若是在Cocoa程序,你也可以使用NSRunLoop 的方法運行run loop,無需檢查返回值。(關於使用NSRunLoop返回運行run loop的例子,查看列表3-12)

Listing 3-2  Running a run loop

- (void)threadMain
{
    // The application uses garbage collection, so no autorelease pool is needed.
    NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

    // Create a run loop observer and attach it to the 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);
    }

    // Create and schedule the timer.
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
                selector:@selector(doFireTimer:) userInfo:nil repeats:YES];

    NSInteger    loopCount = 10;
    do
    {
        // Run the run loop 10 times to let the timer fire.
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
        loopCount--;
    }
    while (loopCount);
}
 

可以遞歸的運行run loop。換句話說你可以使用CFRunLoopRun,CFRunLoopRunInMode或者任一NSRunLoop的方法在輸入源或定時器的處理程序裏面啓動run loop。這樣做的話,你可以使用任何模式啓動嵌套的run loop,包括被外層run loop使用的模式。

1.3.4    退出Run Loop

有兩種方法可以讓run loop處理事件之前退出:

  • 給run loop設置超時時間
  • 通知run loop停止

如果可以配置的話,推薦使用第一種方法。指定一個超時時間可以使run loop退出前完成所有正常操作,包括髮送消息給run loop觀察者。

使用CFRunLoopStop來顯式的停止run loop和使用超時時間產生的結果相似。Run loop把所有剩餘的通知發送出去再退出。與設置超時的不同的是你可以在無條件啓動的run loop裏面使用該技術。

儘管移除run loop的輸入源和定時器也可能導致run loop退出,但這並不是可靠的退出run loop的方法。一些系統例程會添加輸入源到run loop裏面來處理所需事件。因爲你的代碼未必會考慮到這些輸入源,這樣可能導致你無法沒從系統例程中移除它們,從而導致退出run loop。

1.3.5    線程安全和Run Loop對象

線程是否安全取決於你使用那些API來操縱你的run loop。Core Foundation 中的函數通常是線程安全的,可以被任意線程調用。但是如果你修改了run loop的配置然後需要執行某些操作,任何時候你最好還是在run loop所屬的線程執行這些操作。

至於Cocoa的NSRunLoop類則不像Core Foundation具有與生俱來的線程安全性。如果你想使用NSRunLoop類來修改你的run loop,你應用在run loop所屬的線程裏面完成這些操作。給屬於不同線程的run loop添加輸入源和定時器有可能導致你的代碼崩潰或產生不可預知的行爲。

1.4        配置Run loop 的源

以下部分列舉了在Cocoa和Core Foundation裏面如何設置不同類型的輸入源的例子。

1.4.1    定義自定義輸入源

創建自定義的輸入源包括定義以下內容:

  1. 輸入源要處理的信息。
  2. 使感興趣的客戶端(可理解爲其他線程)知道如何和輸入源交互的調度例程。
  3. 處理其他任何客戶端(可理解爲其他線程)發送請求的例程。
  4. 使輸入源失效的取消例程。

由於你自己創建輸入源來處理自定義消息,實際配置選是靈活配置的。調度例程,處理例程和取消例程都是你創建自定義輸入源時最關鍵的例程。然而輸入源其他的大部分行爲都發生在這些例程的外部。比如,由你決定數據傳輸到輸入源的機制,還有輸入源和其他線程的通信機制也是由你決定。

圖3-2顯示了一個自定義輸入源的配置的例子。在該例中,程序的主線程維護了輸入源的引用,輸入源所需的自定義命令緩衝區和輸入源所在的run loop。當主線程有任務需要分發給工作線程時,主線程會給命令緩衝區發送命令和必須的信息來通知工作線程開始執行任務。(因爲主線程和輸入源所在工作線程都可以訪問命令緩衝區,因此這些訪問必須是同步的)一旦命令傳送出去,主線程會通知輸入源並且喚醒工作線程的run loop。而一收到喚醒命令,run loop會調用輸入源的處理程序,由它來執行命令緩衝區中相應的命令。

Figure 3-2  Operating a custom input source

 

以下部分解釋下上圖的實現自定義輸入源關鍵部分和你需要實現的關鍵代碼。

定義輸入源

定義自定義的輸入源需要使用Core Foundation的例程來配置你的run loop源並把它添加到run loop。儘管這些基本的處理例程是基於C的函數,但並不排除你可以對這些函數進行封裝,並使用Objective-C或Objective-C++來實現你代碼的主體。

圖3-2中的輸入源使用了Objective-C的對象輔助run loop來管理命令緩衝區。列表3-3給出了該對象的定義。RunLoopSource對象管理着命令緩衝區並以此來接收其他線程的消息。例子同樣給出了RunLoopContext對象的定義,它是一個用於傳遞RunLoopSource對象和run loop引用給程序主線程的一個容器。

Listing 3-3  The custom input source object definition

@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代碼來管理輸入源的自定義數據,但是將輸入源附加到run loop卻需要使用基於C的回調函數。當你正在把你的run loop源附加到run loop的時候,使用列表3-4中的第一個函數(RunLoopSourceScheduleRoutine)。因爲這個輸入源只有一個客戶端(即主線程),它使用調度函數發送註冊信息給應用程序的委託(delegate)。當委託需要和輸入源通信時,它會使用RunLoopContext對象來完成。

Listing 3-4  Scheduling a run loop source

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

 

一個最重要的回調例程就在輸入源被告知時用來處理自定義數據的那個例程。列表3-5顯示瞭如何調用這個和RunLoopSource對象相關回調例程。這裏只是簡單的讓RunLoopSource執行sourceFired方法,然後繼續處理在命令緩存區出現的命令。

Listing 3-5  Performing work in the input source

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


如果你使用CFRunLoopSourceInvalidate函數把輸入源從run loop裏面移除的話,系統會調用你輸入源的取消例程。你可以使用該例程來通知其他客戶端該輸入源已經失效,客戶端應該釋放輸入源的引用。列表3-6顯示了由已註冊的RunLoopSource對取消例程的調用。這個函數將另一個RunLoopContext對象發送給應用的委託,當這次是要通知委託釋放run loop源的引用。

Listing 3-6  Invalidating an input source

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:方法將在”協調客輸入源的客戶端”部分介紹。

 

安裝輸入源到Run Loop

列表3-7顯示了RunLoopSource的init和addToCurrentRunLoop的方法。Init方法創建CFRunLoopSourceRef的不透明類型,該類型必須被附加到run loop裏面。它把RunLoopSource對象做爲上下文引用參數,以便回調例程持有該對象的一個引用指針。輸入源的安裝只在工作線程調用addToCurrentRunLoop方法才發生,此時RunLoopSourceScheduledRoutine被調用。一旦輸入源被添加到run loop,線程就運行run loop並等待事件。

Listing 3-7  Installing the run loop source

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

協調輸入源的客戶端

爲了讓添加的輸入源有用,你需要維護它並從其他線程給它發送信號。輸入源的主要工作就是將與輸入源相關的線程置於休眠狀態直到有事件發生。這就意味着程序中的要有其他線程知道該輸入源信息並有辦法與之通信。

通知客戶端關於你輸入源信息的方法之一就是當你的輸入源開始安裝到你的run loop上面後發送註冊請求。你把輸入源註冊到任意數量的客戶端,或者通過由代理將輸入源註冊到感興趣的客戶端那。列表3-8顯示了應用委託定義的註冊方法以及它在RunLoopSource對象的調度函數被調用時如何運行。該方法接收RunLoopSource提供的RunLoopContext對象,然後將其添加到它自己的源列表裏面。另外,還顯示了輸入源從run loop移除時候的使用來取消註冊例程。

Listing 3-8  Registering and removing an input source with the application delegate

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


注意:該回調函數調用了列表3-4和列表3-6中描述的方法。

 

通知輸入源

在客戶端發送數據到輸入源後,它必鬚髮信號通知源並且喚醒它的run loop。發送信號給源可以讓run loop知道該源已經做好處理消息的準備。而且因爲信號發送時線程可能處於休眠狀態,你必須總是顯式的喚醒run loop。如果不這樣做的話會導致延遲處理輸入源。

列表3-9顯示了RunLoopSource對象的fireCommandsOnRunLoop方法。當客戶端準備好處理加入緩衝區的命令後會調用此方法。

Listing 3-9  Waking up the run loop

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


注意:你不應該試圖通過自定義輸入源處理一個SIGHUP或其他進程級別類型的信號。Core Foundation喚醒run loop的函數不是信號安全的,不能在你的應用信號處理例程(signal handler routines)裏面使用。關於更多信號處理例程,參閱sigaction主頁。

1.4.2    配置定時源

爲了創建一個定時源,你所需要做只是創建一個定時器對象並把它調度到你的run loop。Cocoa程序中使用NSTimer類來創建一個新的定時器對象,而Core Foundation中使用CFRunLoopTimerRef不透明類型。本質上,NSTimer類是Core Foundation的簡單擴展,它提供了便利的特徵,例如能使用相同的方法創建和調配定時器。

Cocoa中可以使用以下NSTimer類方法來創建並調配一個定時器:

scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:

scheduledTimerWithTimeInterval:invocation:repeats:

上述方法創建了定時器並以默認模式把它們添加到當前線程的run loop。你可以手工的創建NSTimer對象,並通過NSRunLoop的addTimer:forMode:把它添加到run loop。兩種方法都做了相同的事,區別在於你對定時器配置的控制權。例如,如果你手工創建定時器並把它添加到run loop,你可以選擇要添加的模式而不使用默認模式。列表3-10顯示瞭如何使用這這兩種方法創建定時器。第一個定時器在初始化後1秒開始運行,此後每隔0.1秒運行。第二個定時器則在初始化後0.2秒開始運行,此後每隔0.2秒運行。

Listing 3-10  Creating and scheduling timers using NSTimer

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函數來配置定時器的代碼。儘管這個例子中並沒有把任何用戶定義的信息作爲上下文結構,但是你可以使用這個上下文結構傳遞任何你想傳遞的信息給定時器。關於該上下文結構的內容的詳細信息,參閱CFRunLoopTimer Reference。

Listing 3-11  Creating and scheduling a timer using Core Foundation

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


1.4.3    配置基於端口的輸入源

Cocoa和Core Foundation都提供了基於端口的對象用於線程或進程間的通信。以下部分顯示如何使用幾種不同類型的端口對象建立端口通信。

配置NSMachPort對象

爲了和NSMachPort對象建立穩定的本地連接,你需要創建端口對象並將之加入相應的線程的run loop。當運行輔助線程的時候,你傳遞端口對象到線程的主體入口點。輔助線程可以使用相同的端口對象將消息返回給原線程。

a)  實現主線程的代碼

列表3-12顯示了加載輔助線程的主線程代碼。因爲Cocoa框架執行許多配置端口和run loop相關的步驟,所以lauchThread方法比相應的Core Foundation版本(列表3-17)要明顯簡短。然而兩種方法的本質幾乎是一樣的,唯一的區別就是在Cocoa中直接發送NSPort對象,而不是發送本地端口名稱。

Listing 3-12  Main thread launch method

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


爲了在你的線程間建立雙向的通信,你需要讓你的工作線程在簽到的消息中發送自己的本地端口到主線程。主線程接收到簽到消息後就可以知道輔助線程運行正常,並且提供了發送消息給輔助線程的方法。

列表3-13顯示了主要線程的handlePortMessage:方法。當由數據到達線程的本地端口時,該方法被調用。當簽到消息到達時,此方法可以直接從輔助線程裏面檢索端口並保存下來以備後續使用。

Listing 3-13  Handling Mach port messages

#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.
    }
}


b)  輔助線程的實現代碼

對於輔助工作線程,你必須配置線程使用特定的端口以發送消息返回給主要線程。

列表3-14顯示瞭如何設置工作線程的代碼。創建了線程的自動釋放池後,緊接着創建工作對象驅動線程運行。工作對象的sendCheckinMessage:方法(如列表3-15所示)創建了工作線程的本地端口併發送簽到消息回主線程。

Listing 3-14  Launching the worker thread using Mach ports

+(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時候,本地和遠程線程可以使用相同的端口對象在線程間進行單邊通信。換句話說,一個線程創建的本地端口對象成爲另一個線程的遠程端口對象。

列表3-15顯示了輔助線程的簽到例程,該方法爲之後的通信設置自己的本地端口,然後發送簽到消息給主線程。它使用LaunchThreadWithPort:方法中收到的端口對象做爲目標消息。

Listing 3-15  Sending the check-in message using Mach ports

// 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對象

爲了和NSMeaasgePort的建立穩定的本地連接,你不能簡單的在線程間傳遞端口對象。遠程消息端口必須通過名字來獲得。在Cocoa中這需要你給本地端口指定一個名字,並將名字傳遞到遠程線程以便遠程線程可以獲得合適的端口對象用於通信。列表3-16顯示端口創建,註冊到你想要使用消息端口的進程。

Listing 3-16  Registering a message port

 
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顯示了程序主線程加載工作線程的代碼。第一步是設置CFMessagePortRef不透明類型來監聽工作線程的消息。工作線程需要端口的名稱來建立連接,以便使字符串傳遞給工作線程的主入口函數。在當前的用戶上下文中端口名必須是唯一的,否則可能在運行時造成衝突。

Listing 3-17  Attaching a Core Foundation message port to a new thread

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


端口建立而且線程啓動後,主線程在等待線程簽到時可以繼續執行。當簽到消息到達後,主線程使用MainThreadResponseHandler來分發消息,如列表3-18所示。這個函數提取工作線程的端口名,並創建用於未來通信的管道。

Listing 3-18  Receiving the checkin message

#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;
}


主線程配置好後,剩下的唯一事情是讓新創建的工作線程創建自己的端口然後簽到。列表3-19顯示了工作線程的入口函數。函數獲取了主線程的端口名並使用它來創建和主線程的遠程連接。然後這個函數創建自己的本地端口號,安裝到線程的run loop,最後連同本地端口名稱一起發回主線程簽到。

Listing 3-19  Setting up the thread structures

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,所有發送到線程端口的事件都會由ProcessClientRequest函數處理。函數的具體實現依賴於線程的工作方式,這裏就不舉例了。






http://www.dreamingwish.com/dream-2012/ios-multithread-program-runloop-the.html




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章