本附錄描述了Mac OS X和iOS上面一些關鍵的進階安全執行緒的架構。本附錄的資訊有可能會發生改變。 Cocoa
在Cocoa上面使用多線程的指南包括以下這些: 不可改變的對象一般是安全執行緒的。一旦你建立了它們,你可以把這些對象線上程間安全的傳遞。另一方面,可變對象通常不是安全執行緒的。為了在多線程應用裡面使用可變對象,應用必須適當的同步。關於更多資訊,參閱”可變和不可變對比”。 許多個物件在多線程裡面不安全的使用被視為是”線程不安全的”。只要同一時間只有一個線程,那麼許多這些對象可以被多個線程使用。這種被稱為專門限制應用程式的主線程的對象通常被這樣調用。 應用的主線程負責處理事件。儘管Application Kit在其他線程被包含在事件路徑裡面時還會繼續工作,但操作可能會被打亂順序。 如果你想使用一個線程來繪畫一個視圖,把所有繪畫的代碼放在NSView的lockFocusIfCanDraw和unlockFocus方法中間。
為了在Cocoa裡面使用POSIX線程,你必須首先把Cocoa變為多線程模式。關於更多資訊,參閱“在Cocoa應用裡面使用POSIX線程”部分。 基礎架構(Fondation Framework)的安全執行緒
有一種誤解,認為基礎架構(Foundation framework)是安全執行緒的,而Application Kit是非安全執行緒的。不幸的是,這是一個總的概括,從而造成一點誤導。每個架構都包含了安全執行緒部分和非安全執行緒部分。以下部分介紹Foundation framework裡面的安全執行緒部分。
安全執行緒的類和函數
下面這些類和函數通常被認為是安全執行緒的。你可以在多個線程裡面使用它們的同一個執行個體,而無需擷取一個鎖。 NSArray NSAssertionHandler NSAttributedString NSCalendarDate NSCharacterSet NSConditionLock NSConnection NSData NSDate NSDecimal functions NSDecimalNumber NSDecimalNumberHandler NSDeserializer NSDictionary NSDistantObject NSDistributedLock NSDistributedNotificationCenter NSException NSFileManager (in Mac OS X v10.5 and later) NSHost NSLock NSLog/NSLogv NSMethodSignature NSNotification NSNotificationCenter NSNumber NSObject NSPortCoder NSPortMessage NSPortNameServer NSProtocolChecker NSProxy NSRecursiveLock NSSet NSString NSThread NSTimer NSTimeZone NSUserDefaults NSValue 還有對象的allocation和retain函數 Zone和記憶體函數
非安全執行緒類
以下這些類和函數通常被認為是非安全執行緒的。在大部分情況下,你可以在任何線程裡面使用這些類,只要你在同一個時間只在一個線程裡面使用它們。參考這些類對於的額外詳細資料的文檔。 NSArchiver NSAutoreleasePool NSBundle NSCalendar NSCoder NSCountedSet NSDateFormatter NSEnumerator NSFileHandle NSFormatter NSHashTable functions NSInvocation NSJavaSetup functions NSMapTable functions NSMutableArray NSMutableAttributedString NSMutableCharacterSet NSMutableData NSMutableDictionary NSMutableSet NSMutableString NSNotificationQueue NSNumberFormatter NSPipe NSPort NSProcessInfo NSRunLoop NSScanner NSSerializer NSTask NSUnarchiver NSUndoManager User name and home directory functions
注意,儘管NSSerializer,NSArchiver,NSCoder和NSEnumerator對象本身是安全執行緒的,但是它們被放置這這裡是因為當它們封裝的對象被使用的時候,更改這些對象資料是不安全的。比如,在歸檔情況下,修改被歸檔的對象是不安全的。對於一個枚舉,任何線程修改枚舉的集合都是不安全的。
只能用於主線程的類
以下的類必須只能在應用的主線程類使用。 NSAppleScript
可變 vs 不可變
不可變對象通常是安全執行緒的。一旦你建立了它們,你可以把它們安全的線上程間傳遞。當前,在使用不可變對象時,你還應該記得正確使用引用計數。如果不適當的釋放了一個你沒有引用的對象,你在隨後有可能造成一個異常。
可變對象通常是非安全執行緒的。為了在多線程應用裡面使用可變對象,應用應該使用鎖來同步訪問它們(關於更多資訊,參見“原子操作”部分)。通常情況下,集合類(比如,NSMutableArray,NSMutableDictionary)是考慮多變時是非安全執行緒的。這意味著,如果一個或多個線程同時改變一個數組,將會發生問題。你應該線上程讀取和寫入它們的地方使用鎖包圍著。
即使一個方法要求返回一個不可變對象,你不應該簡單的假設返回的對象就是不可變的。依賴於方法的實現,返回的對象有可能是可變的或著不可變的。比如,一個傳回型別是NSString的方法有可能實際上由於它的實現返回了一個NSMutableString。如果你想要確保對象是不可變的,你應該使用不可變的拷貝。
可重新進入性
可重新進入性是可以讓同一對象或者不同對象上一個操作“調用”其他動作成為可能。保持和釋放對象就是一個有可能被忽視的”調用”的例子。
以下列表列出了Foundation framework的部分顯式的可重新進入對象。所有其他類可能是或可能不是可重新進入的,或者它們將來有可能是可重新進入的。對於可重新進入性的一個完整的分析是不可能完成的,而且該列表將會是無窮盡的。 Distributed Objects NSConditionLock NSDistributedLock NSLock NSLog/NSLogv NSNotificationCenter NSRecursiveLock NSRunLoop NSUserDefaults
類的初始化
Objective-C的運行時系統在類收到其他任何訊息之前給它發送一個initialize訊息。這可以讓類有機會在它被使用前設定它的運行時環境。在一個多線程應用裡面,運行時保證僅有一個線程(該線程恰好發送第一條訊息給類)執行initialized方法,第二個線程阻塞直到第一個線程的initialize方法執行完成。在此期間,第一個線程可以繼續調用其他類上的方法。該initialize方法不應該依賴於第二個線程對這個類的調用。如果不是這樣的話,兩個線程將會造成死結。
自動釋放池(Autorelease Pools)
每個線程都維護它自己的NSAutoreleasePool的棧對象。Cocoa希望在每個當前線程的棧裡面有一個可用的自動釋放池。如果一個自動釋放池不可用,對象將不會給釋放,從而造成記憶體泄露。對於Application Kit的主線程通常它會自動建立並消耗一個自動釋放池,但是輔助線程(和其他只有Foundationd的程式)在使用Cocoa前必須自己手工建立。如果你的線程是長時間啟動並執行,那麼有可能潛在產生很多自動釋放的對象,你應該周期性的銷毀它們並建立自動釋放池(就像Application Kit對主線程那樣)。否則,自動釋放對象將會積累並造成記憶體大量佔用。如果你的脫離線程沒有使用Cocoa,你不需要建立一個自動釋放池。
Run Loops
每個線程都有一個或多個run loop。然而每個run loop和每個線程都有它自己的輸入模式來決定run loop啟動並執行釋放監聽那些輸入源。輸入模式定義在一個run loop上面,不會影響定義在其他run loop的輸入模式,即使它們的名字相同。
如果你的線程是基於Application Kti的話,主線程的run loop會自動運行,但是輔助線程(和只有Foundation的應用)必須自己啟動它們的run loop。如果一個脫離線程沒有進入run loop,那麼線程在完成它們的方法執行後會立即退出。
儘管外表顯式可能是安全執行緒的,但是NSRunLoop類是非安全執行緒的。你只能在擁有它們的線程裡面調用它執行個體的方法。 Application Kit架構的安全執行緒
以下部分介紹了Application Kit架構的安全執行緒。
非安全執行緒類
以下這些類和函數通常是非安全執行緒的。大部分情況下,你可以在任何線程使用這些類,只要你在同一時間只有一個線程使用它們。查看這些類的文檔來獲得更多的詳細資料。 NSGraphicsContext。多資訊,參見“NSGraphicsContext 限制”。 NSImage.更多資訊,參見“NSImage 限制”。 NSResponder。 NSWindow和所有它的子類。更多資訊,參見“Window 限制
只能用於主線程的類
以下的類必須只能在應用的主線程使用。 NSCell和所有它的子類。 NSView和所有它的子類。更多資訊,參見“NSView 限制”。
Window 限制
你可以在輔助線程建立一個window。Application Kit確保和window相關的資料結構在主線程釋放來避免產生條件。在同時包含大量windows的應用中,window對象有可能會發生泄漏。
你也可以在輔助線程建立modal window。在主線程運行modal loop時,Application Kit阻塞輔助線程的調用。
事件處理常式限制
應用的主線程負責處理事件。主線程阻塞在NSApplication的run方法,通常該方法被包含在main函數裡面。在Application Kit繼續工作時,如果其他線程被包含在事件路徑,那麼操作有可能打亂順序。比如,如果兩個不同的線程負責關鍵事件,那麼關鍵事件有可能不是按照順序到達。通過讓主線程來處理事件,事件可以被分配到輔助線程由它們處理。
你可以在輔助線程裡面使用NSApplication的postEvent:atStart方法傳遞一個事件給主線程的事件隊列。然而,順序不能保證和使用者輸入的事件順序相同。應用的主線程仍然輔助處理事件隊列的事件。
繪畫限制
Application Kit在使用它的繪畫函數和類時通常是安全執行緒的,包括NSBezierPath和NSString類。關於使用這些類的詳細資料,在以下各部分介紹。關於繪畫的額外資訊和線程可以查看Cocoa Drawing Guide。
a) NSView限制
NSView通常是安全執行緒的,包含幾個異常。你應該僅在應用的主線程裡面執行對NSView的建立、銷毀、調整大小、移動和其他動作。在其他輔助線程裡面只要你把繪畫的代碼放在lockFocusIfCanDraw和unlockFocus方法之間也是安全執行緒的。
如果應用的輔助線程想要告知主線程重繪視圖,一定不能在輔助線程直接調用display,setNeedsDisplay:,setNeedsDisplayInRect:,或setViewsNeedDisplay:方法。相反,你應該給給主線程發生一個訊息讓它調用這些方法,或者使用performSelectorOnMainThread:withObject:waitUntilDone:方法。
系統檢視表的圖形狀態(gstates)是基於每個線程不同的。使用圖形狀態可以在單線程的應用裡面獲得更好的繪畫效能,但是現在已經不是這樣了。不正確使用圖形狀態可能導致主線程的繪畫代碼更低效。
b) NSGraphicsContext 限制
NSGraphicsContext類代表了繪畫上下文,它由底層繪畫系統提供。每個NSGraphicsContext執行個體都擁有它獨立的繪畫狀態:座標系統、裁剪、當前字型等。該類的執行個體在主線程自動建立自己的NSWindow執行個體。如果你在任何輔助線程執行繪畫操作,需要特定為該線程建立一個新的NSGraphicsContext執行個體。
如果你在任何輔助線程執行繪畫,你必須手工的重新整理繪畫調用。Cocoa不會自動更新輔助線程繪畫的內容,所以你當你完成繪畫後需要調用NSGraphicsContext的flusGrahics方法。如果你的應用程式只在主線程繪畫,你不需要重新整理繪畫調用。
c) NSImage限制
線程可以建立NSImage對象,把它繪畫到圖片緩衝區,還可以把它傳遞給主線程來繪畫。底層的圖片緩衝被所有線程共用。關於圖片和如何緩衝的更多資訊,參閱Ccocoa Drawing Guide。 Core Data架構
Core Data架構通常支援多線程,儘管需要注意一些使用注意事項。關於這些注意事項的更多資訊,參閱Core Data Programing Guide的“Multi-Threading with Core Data”部分。 Core Foundation(核心架構)
Core Foundation是足夠安全執行緒的,如果你的程式注意一下的話,應該不會遇到任何線程競爭的問題。通常情況下是安全執行緒的,比如當你查詢(query)、引用(retain)、釋放(release)和傳遞(pass)不可變對象時。甚至在多個線程查詢中央共用對象也是安全執行緒的。
像Cocoa那樣,當涉及對象或它們內容突變時,Core Foundation是非安全執行緒的。比如,正如你所期望的,無論修改一個可變資料或可變數組對象,還是修改一個可變數組裡面的對象都是非安全執行緒的。其中一個原因是效能,這是在這種情況下的關鍵。此外,在該層級上實現完全安全執行緒是幾乎不可能的。例如,你不能排除從集合中引用(retain)一個對象產生的無法確定的結果。該集合本身在被調用來引用(retain)它所包含的對象之前有可能已經被釋放了。
這些情況下,當你的對象被多個線程訪問或修改,你的代碼應該在相應的地方使用鎖來保護它們不要被同時訪問。例如,枚舉Core Foundation數組對象的代碼,在枚舉塊代碼周圍應該使用合適的鎖來保護它免遭其他線程修改。
術語表
應用(application)
一個顯示一個圖形化使用者介面給使用者的特定樣式程式。
條件(condition)
一個用來同步資源訪問的結構。線程等待某一條件來決定是否被允許繼續運行,直到其他線程顯式的給該條件發送訊號。
臨界區(critical section)
同一時間只能不被一個線程執行的代碼。
輸入源(input source)
一個線程的非同步事件源。輸入源可以是基於連接埠的或手工觸發,並且必須被附加到某一個線程的run loop上面。
可串連的線程(join thread)
退出時資源不會被立即回收的線程。可串連的線程在資源被回收之前必須被顯式脫離或由其他線程串連。可連接線程提供了一個傳回值給串連它的線程。
主線程(main thread)
當建立進程時一起建立的特定類型的線程。當程式的主線程退出,則程式即退出。
互斥鎖(mutex)
提供共用資源互斥訪問的鎖。一個互斥鎖同一時間只能被一個線程擁有。試圖擷取一個已經被其他線程擁有的互斥鎖,會把當前線程置於休眠狀態知道該鎖被其他線程釋放並讓當前線程獲得。
操作對象(operation object)
NSOperation類的執行個體。操作對象封裝了和某一任務相關的代碼和資料到一個執行單元裡面。
操作隊列(operation queue)
NSOperationQueue類的執行個體。操作隊列管理操作對象的執行。
進程(process)
應用或程式的運行時執行個體。一個進程擁有獨立於分配給其他程式的的記憶體空間和系統資源(包括連接埠許可權)。進程總是包含至少一個線程(即主線程)和任意數量的額外線程。
程式(program)
可以用來執行某些任務的代碼和資源的組合。程式不需要一個圖形化使用者介面,儘管圖形應用也被稱為程式。
遞迴鎖(recursive lock)
可以被同一線程多次鎖住的鎖。
Run loop(運行迴圈)
一個事件處理迴圈,在此期間事件被接收並分配給合適的處理常式。
Run loop模式(run loop mode)
與某一特定名稱相關的輸入源、定時源和run loop觀察者的集合。當運行在某一特定“模式”下,一個run loop監視和該模式相關的源和觀察者。
Run loop對象(run loop object)
NSRunLoop類或CFRunLoopRef不透明類型的執行個體。這些對象提供線程裡面實現事件處理迴圈的介面。
Run loop觀察者(run loop observer)
在run loop啟動並執行不同階段時接收通知的對象。
訊號量(semaphore)
一個受保護的變數,它限制共用資源的訪問。互斥鎖(mutexes)和條件(conditions)都是不同類型的訊號量。
任務(task)
要執行的工作數量。儘管一些技術(最顯著的是Carbon 多進程服務—Carbon Multiprocessing Services)使用該術語的意義有時不同,但是最通用的用法是表明需要執行的工作數量的抽象概念。
線程(thread)
進程裡面的一個執行過程流。每個線程都有它自己的棧空間,但除此之外同一進程的其他線程共用記憶體。
定時源(timer source)
為線程同步事件的源。定時器產生預定時間將要執行的一次或重複事件。
結束語
多線程編程在開發應用的時候非常有協助。比如你可以在後台載入圖片,等圖片載入完成後再在主線程更新等,或者在幕後處理一些需要佔用CPU很長時間的事件(比如請求伺服器,載入資料等)。要體會多線程編程的好處,還得多實戰,結合使用多種多線程技術。特別要注意Run Loop的使用,很多開發人員在編寫多線程應用的時候很少關注過Run Loop。如果你仔細閱讀並掌握Run Loop的細節,將會協助你寫出更優美的代碼。同步是多線程編程的老生常談,估計大學時候大家都基本熟悉了同步的重要性。
最後,本文在翻譯過程中發現很多地方直譯成中文比較晦澀,所以採用了意譯的方式,這不可避免的造成有一些地方可能和原文有一定的出入,所以如果你閱讀的時候發現有任何的錯誤都可以給我發郵件:xyl.layne(a)gmail.com。
最後可以關注我微博大家一起溝通交流學習。
微博地址: http://weibo.com/u/1826448972