iOS多線程編程指南(拓展篇)
本拓展篇描述了Mac OS X和iOS上面一些關(guān)鍵的高級(jí)線程安全的框架。文中的信息有可能會(huì)發(fā)生改變。
一、Cocoa
在Cocoa上面使用多線程的指南包括以下這些:
(1)不可改變的對(duì)象一般是線程安全的。一旦你創(chuàng)建了它們,你可以把這些對(duì)象在線程間安全的傳遞。另一方面,可變對(duì)象通常不是線程安全的。為了在多線程應(yīng)用里面使用可變對(duì)象,應(yīng)用必須適當(dāng)?shù)耐?。關(guān)于更多信息,參閱”可變和不可變對(duì)比”。
(2)許多對(duì)象在多線程里面不安全的使用被視為是”線程不安全的”。只要同一時(shí)間只有一個(gè)線程,那么許多這些對(duì)象可以被多個(gè)線程使用。這種被稱為專門限制應(yīng)用程序的主線程的對(duì)象通常被這樣調(diào)用。
(3)應(yīng)用的主線程負(fù)責(zé)處理事件。盡管Application Kit在其他線程被包含在事件路徑里面時(shí)還會(huì)繼續(xù)工作,但操作可能會(huì)被打亂順序。
(4)如果你想使用一個(gè)線程來繪畫一個(gè)視圖,把所有繪畫的代碼放在NSView的lockFocusIfCanDraw和unlockFocus方法中間。
為了在Cocoa里面使用POSIX線程,你必須首先把Cocoa變?yōu)槎嗑€程模式。關(guān)于更多信息,參閱“在Cocoa應(yīng)用里面使用POSIX線程”部分。
基礎(chǔ)框架(Fondation Framework)的線程安全
有一種誤解,認(rèn)為基礎(chǔ)框架(Foundation framework)是線程安全的,而Application Kit是非線程安全的。不幸的是,這是一個(gè)總的概括,從而造成一點(diǎn)誤導(dǎo)。每個(gè)框架都包含了線程安全部分和非線程安全部分。以下部分介紹Foundation framework里面的線程安全部分。
線程安全的類和函數(shù)
下面這些類和函數(shù)通常被認(rèn)為是線程安全的。你可以在多個(gè)線程里面使用它們的同一個(gè)實(shí)例,而無需獲取一個(gè)鎖。
- 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
- 還有對(duì)象的allocation和retain函數(shù)
- Zone和內(nèi)存函數(shù)
非線程安全類
以下這些類和函數(shù)通常被認(rèn)為是非線程安全的。在大部分情況下,你可以在任何線程里面使用這些類,只要你在同一個(gè)時(shí)間只在一個(gè)線程里面使用它們。參考這些類對(duì)于的額外詳細(xì)信息的文檔。
- 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對(duì)象本身是線程安全的,但是它們被放置這這里是因?yàn)楫?dāng)它們封裝的對(duì)象被使用的時(shí)候,更改這些對(duì)象數(shù)據(jù)是不安全的。比如,在歸檔情況下,修改被歸檔的對(duì)象是不安全的。對(duì)于一個(gè)枚舉,任何線程修改枚舉的集合都是不安全的。
只能用于主線程的類
以下的類必須只能在應(yīng)用的主線程類使用。
- NSAppleScript
可變 vs 不可變
不可變對(duì)象通常是線程安全的。一旦你創(chuàng)建了它們,你可以把它們安全的在線程間傳遞。當(dāng)前,在使用不可變對(duì)象時(shí),你還應(yīng)該記得正確使用引用計(jì)數(shù)。如果不適當(dāng)?shù)尼尫帕艘粋€(gè)你沒有引用的對(duì)象,你在隨后有可能造成一個(gè)異常。
可變對(duì)象通常是非線程安全的。為了在多線程應(yīng)用里面使用可變對(duì)象,應(yīng)用應(yīng)該使用鎖來同步訪問它們(關(guān)于更多信息,參見“原子操作”部分)。通常情況下,集合類(比如,NSMutableArray,NSMutableDictionary)是考慮多變時(shí)是非線程安全的。這意味著,如果一個(gè)或多個(gè)線程同時(shí)改變一個(gè)數(shù)組,將會(huì)發(fā)生問題。你應(yīng)該在線程讀取和寫入它們的地方使用鎖包圍著。
即使一個(gè)方法要求返回一個(gè)不可變對(duì)象,你不應(yīng)該簡單的假設(shè)返回的對(duì)象就是不可變的。依賴于方法的實(shí)現(xiàn),返回的對(duì)象有可能是可變的或著不可變的。比如,一個(gè)返回類型是NSString的方法有可能實(shí)際上由于它的實(shí)現(xiàn)返回了一個(gè)NSMutableString。如果你想要確保對(duì)象是不可變的,你應(yīng)該使用不可變的拷貝。
可重入性
可重入性是可以讓同一對(duì)象或者不同對(duì)象上一個(gè)操作“調(diào)用”其他操作成為可能。保持和釋放對(duì)象就是一個(gè)有可能被忽視的”調(diào)用”的例子。
以下列表列出了Foundation framework的部分顯式的可重入對(duì)象。所有其他類可能是或可能不是可重入的,或者它們將來有可能是可重入的。對(duì)于可重入性的一個(gè)完整的分析是不可能完成的,而且該列表將會(huì)是無窮盡的。
- Distributed Objects
- NSConditionLock
- NSDistributedLock
- NSLock
- NSLog/NSLogv
- NSNotificationCenter
- NSRecursiveLock
- NSRunLoop
- NSUserDefaults
類的初始化
Objective-C的運(yùn)行時(shí)系統(tǒng)在類收到其他任何消息之前給它發(fā)送一個(gè)initialize消息。這可以讓類有機(jī)會(huì)在它被使用前設(shè)置它的運(yùn)行時(shí)環(huán)境。在一個(gè)多線程應(yīng)用里面,運(yùn)行時(shí)保證僅有一個(gè)線程(該線程恰好發(fā)送第一條消息給類)執(zhí)行initialized方法,第二個(gè)線程阻塞直到第一個(gè)線程的initialize方法執(zhí)行完成。在此期間,第一個(gè)線程可以繼續(xù)調(diào)用其他類上的方法。該initialize方法不應(yīng)該依賴于第二個(gè)線程對(duì)這個(gè)類的調(diào)用。如果不是這樣的話,兩個(gè)線程將會(huì)造成死鎖。
自動(dòng)釋放池(Autorelease Pools)
每個(gè)線程都維護(hù)它自己的NSAutoreleasePool的棧對(duì)象。Cocoa希望在每個(gè)當(dāng)前線程的棧里面有一個(gè)可用的自動(dòng)釋放池。如果一個(gè)自動(dòng)釋放池不可用,對(duì)象將不會(huì)給釋放,從而造成內(nèi)存泄露。對(duì)于Application Kit的主線程通常它會(huì)自動(dòng)創(chuàng)建并消耗一個(gè)自動(dòng)釋放池,但是輔助線程(和其他只有Foundationd的程序)在使用Cocoa前必須自己手工創(chuàng)建。如果你的線程是長時(shí)間運(yùn)行的,那么有可能潛在產(chǎn)生很多自動(dòng)釋放的對(duì)象,你應(yīng)該周期性的銷毀它們并創(chuàng)建自動(dòng)釋放池(就像Application Kit對(duì)主線程那樣)。否則,自動(dòng)釋放對(duì)象將會(huì)積累并造成內(nèi)存大量占用。如果你的脫離線程沒有使用Cocoa,你不需要?jiǎng)?chuàng)建一個(gè)自動(dòng)釋放池。
Run Loops
每個(gè)線程都有一個(gè)或多個(gè)run loop。然而每個(gè)run loop和每個(gè)線程都有它自己的輸入模式來決定run loop運(yùn)行的釋放監(jiān)聽那些輸入源。輸入模式定義在一個(gè)run loop上面,不會(huì)影響定義在其他run loop的輸入模式,即使它們的名字相同。
如果你的線程是基于Application Kti的話,主線程的run loop會(huì)自動(dòng)運(yùn)行,但是輔助線程(和只有Foundation的應(yīng)用)必須自己啟動(dòng)它們的run loop。如果一個(gè)脫離線程沒有進(jìn)入run loop,那么線程在完成它們的方法執(zhí)行后會(huì)立即退出。
盡管外表顯式可能是線程安全的,但是NSRunLoop類是非線程安全的。你只能在擁有它們的線程里面調(diào)用它實(shí)例的方法。
Application Kit框架的線程安全
以下部分介紹了Application Kit框架的線程安全。
非線程安全類
以下這些類和函數(shù)通常是非線程安全的。大部分情況下,你可以在任何線程使用這些類,只要你在同一時(shí)間只有一個(gè)線程使用它們。查看這些類的文檔來獲得更多的詳細(xì)信息。
- NSGraphicsContext。多信息,參見“NSGraphicsContext 限制”。
- NSImage.更多信息,參見“NSImage 限制”。
- NSResponder。
- NSWindow和所有它的子類。更多信息,參見“Window 限制
只能用于主線程的類
以下的類必須只能在應(yīng)用的主線程使用。
- NSCell和所有它的子類。
- NSView和所有它的子類。更多信息,參見“NSView 限制”。
Window 限制
你可以在輔助線程創(chuàng)建一個(gè)window。Application Kit確保和window相關(guān)的數(shù)據(jù)結(jié)構(gòu)在主線程釋放來避免產(chǎn)生條件。在同時(shí)包含大量windows的應(yīng)用中,window對(duì)象有可能會(huì)發(fā)生泄漏。
你也可以在輔助線程創(chuàng)建modal window。在主線程運(yùn)行modal loop時(shí),Application Kit阻塞輔助線程的調(diào)用。
事件處理例程限制
應(yīng)用的主線程負(fù)責(zé)處理事件。主線程阻塞在NSApplication的run方法,通常該方法被包含在main函數(shù)里面。在Application Kit繼續(xù)工作時(shí),如果其他線程被包含在事件路徑,那么操作有可能打亂順序。比如,如果兩個(gè)不同的線程負(fù)責(zé)關(guān)鍵事件,那么關(guān)鍵事件有可能不是按照順序到達(dá)。通過讓主線程來處理事件,事件可以被分配到輔助線程由它們處理。
你可以在輔助線程里面使用NSApplication的postEvent:atStart方法傳遞一個(gè)事件給主線程的事件隊(duì)列。然而,順序不能保證和用戶輸入的事件順序相同。應(yīng)用的主線程仍然輔助處理事件隊(duì)列的事件。
繪畫限制
Application Kit在使用它的繪畫函數(shù)和類時(shí)通常是線程安全的,包括NSBezierPath和NSString類。關(guān)于使用這些類的詳細(xì)信息,在以下各部分介紹。關(guān)于繪畫的額外信息和線程可以查看Cocoa Drawing Guide。
a) NSView限制
NSView通常是線程安全的,包含幾個(gè)異常。你應(yīng)該僅在應(yīng)用的主線程里面執(zhí)行對(duì)NSView的創(chuàng)建、銷毀、調(diào)整大小、移動(dòng)和其他操作。在其他輔助線程里面只要你把繪畫的代碼放在lockFocusIfCanDraw和unlockFocus方法之間也是線程安全的。
如果應(yīng)用的輔助線程想要告知主線程重繪視圖,一定不能在輔助線程直接調(diào)用display,setNeedsDisplay:,setNeedsDisplayInRect:,或setViewsNeedDisplay:方法。相反,你應(yīng)該給給主線程發(fā)生一個(gè)消息讓它調(diào)用這些方法,或者使用performSelectorOnMainThread:withObject:waitUntilDone:方法。
系統(tǒng)視圖的圖形狀態(tài)(gstates)是基于每個(gè)線程不同的。使用圖形狀態(tài)可以在單線程的應(yīng)用里面獲得更好的繪畫性能,但是現(xiàn)在已經(jīng)不是這樣了。不正確使用圖形狀態(tài)可能導(dǎo)致主線程的繪畫代碼更低效。
b) NSGraphicsContext 限制
NSGraphicsContext類代表了繪畫上下文,它由底層繪畫系統(tǒng)提供。每個(gè)NSGraphicsContext實(shí)例都擁有它獨(dú)立的繪畫狀態(tài):坐標(biāo)系統(tǒng)、裁剪、當(dāng)前字體等。該類的實(shí)例在主線程自動(dòng)創(chuàng)建自己的NSWindow實(shí)例。如果你在任何輔助線程執(zhí)行繪畫操作,需要特定為該線程創(chuàng)建一個(gè)新的NSGraphicsContext實(shí)例。
如果你在任何輔助線程執(zhí)行繪畫,你必須手工的刷新繪畫調(diào)用。Cocoa不會(huì)自動(dòng)更新輔助線程繪畫的內(nèi)容,所以你當(dāng)你完成繪畫后需要調(diào)用NSGraphicsContext的flusGrahics方法。如果你的應(yīng)用程序只在主線程繪畫,你不需要刷新繪畫調(diào)用。
c) NSImage限制
線程可以創(chuàng)建NSImage對(duì)象,把它繪畫到圖片緩沖區(qū),還可以把它傳遞給主線程來繪畫。底層的圖片緩存被所有線程共享。關(guān)于圖片和如何緩存的更多信息,參閱Ccocoa Drawing Guide。
Core Data框架
Core Data框架通常支持多線程,盡管需要注意一些使用注意事項(xiàng)。關(guān)于這些注意事項(xiàng)的更多信息,參閱Core Data Programing Guide的“Multi-Threading with Core Data”部分。
別走開,下頁內(nèi)容更精彩
#p#
二、Core Foundation(核心框架)
Core Foundation是足夠線程安全的,如果你的程序注意一下的話,應(yīng)該不會(huì)遇到任何線程競爭的問題。通常情況下是線程安全的,比如當(dāng)你查詢(query)、引用(retain)、釋放(release)和傳遞(pass)不可變對(duì)象時(shí)。甚至在多個(gè)線程查詢中央共享對(duì)象也是線程安全的。
像Cocoa那樣,當(dāng)涉及對(duì)象或它們內(nèi)容突變時(shí),Core Foundation是非線程安全的。比如,正如你所期望的,無論修改一個(gè)可變數(shù)據(jù)或可變數(shù)組對(duì)象,還是修改一個(gè)可變數(shù)組里面的對(duì)象都是非線程安全的。其中一個(gè)原因是性能,這是在這種情況下的關(guān)鍵。此外,在該級(jí)別上實(shí)現(xiàn)完全線程安全是幾乎不可能的。例如,你不能排除從集合中引用(retain)一個(gè)對(duì)象產(chǎn)生的無法確定的結(jié)果。該集合本身在被調(diào)用來引用(retain)它所包含的對(duì)象之前有可能已經(jīng)被釋放了。
這些情況下,當(dāng)你的對(duì)象被多個(gè)線程訪問或修改,你的代碼應(yīng)該在相應(yīng)的地方使用鎖來保護(hù)它們不要被同時(shí)訪問。例如,枚舉Core Foundation數(shù)組對(duì)象的代碼,在枚舉塊代碼周圍應(yīng)該使用合適的鎖來保護(hù)它免遭其他線程修改。
三、術(shù)語表
應(yīng)用(application)
一個(gè)顯示一個(gè)圖形用戶界面給用戶的特定樣式程序。
條件(condition)
一個(gè)用來同步資源訪問的結(jié)構(gòu)。線程等待某一條件來決定是否被允許繼續(xù)運(yùn)行,直到其他線程顯式的給該條件發(fā)送信號(hào)。
臨界區(qū)(critical section)
同一時(shí)間只能不被一個(gè)線程執(zhí)行的代碼。
輸入源(input source)
一個(gè)線程的異步事件源。輸入源可以是基于端口的或手工觸發(fā),并且必須被附加到某一個(gè)線程的run loop上面。
可連接的線程(join thread)
退出時(shí)資源不會(huì)被立即回收的線程??蛇B接的線程在資源被回收之前必須被顯式脫離或由其他線程連接??蛇B接線程提供了一個(gè)返回值給連接它的線程。
主線程(main thread)
當(dāng)創(chuàng)建進(jìn)程時(shí)一起創(chuàng)建的特定類型的線程。當(dāng)程序的主線程退出,則程序即退出。
互斥鎖(mutex)
提供共享資源互斥訪問的鎖。一個(gè)互斥鎖同一時(shí)間只能被一個(gè)線程擁有。試圖獲取一個(gè)已經(jīng)被其他線程擁有的互斥鎖,會(huì)把當(dāng)前線程置于休眠狀態(tài)知道該鎖被其他線程釋放并讓當(dāng)前線程獲得。
操作對(duì)象(operation object)
NSOperation類的實(shí)例。操作對(duì)象封裝了和某一任務(wù)相關(guān)的代碼和數(shù)據(jù)到一個(gè)執(zhí)行單元里面。
操作隊(duì)列(operation queue)
NSOperationQueue類的實(shí)例。操作隊(duì)列管理操作對(duì)象的執(zhí)行。
進(jìn)程(process)
應(yīng)用或程序的運(yùn)行時(shí)實(shí)例。一個(gè)進(jìn)程擁有獨(dú)立于分配給其他程序的的內(nèi)存空間和系統(tǒng)資源(包括端口權(quán)限)。進(jìn)程總是包含至少一個(gè)線程(即主線程)和任意數(shù)量的額外線程。
程序(program)
可以用來執(zhí)行某些任務(wù)的代碼和資源的組合。程序不需要一個(gè)圖形用戶界面,盡管圖形應(yīng)用也被稱為程序。
遞歸鎖(recursive lock)
可以被同一線程多次鎖住的鎖。
Run loop(運(yùn)行循環(huán))
一個(gè)事件處理循環(huán),在此期間事件被接收并分配給合適的處理例程。
Run loop模式(run loop mode)
與某一特定名稱相關(guān)的輸入源、定時(shí)源和run loop觀察者的集合。當(dāng)運(yùn)行在某一特定“模式”下,一個(gè)run loop監(jiān)視和該模式相關(guān)的源和觀察者。
Run loop對(duì)象(run loop object)
NSRunLoop類或CFRunLoopRef不透明類型的實(shí)例。這些對(duì)象提供線程里面實(shí)現(xiàn)事件處理循環(huán)的接口。
Run loop觀察者(run loop observer)
在run loop運(yùn)行的不同階段時(shí)接收通知的對(duì)象。
信號(hào)量(semaphore)
一個(gè)受保護(hù)的變量,它限制共享資源的訪問?;コ怄i(mutexes)和條件(conditions)都是不同類型的信號(hào)量。
任務(wù)(task)
要執(zhí)行的工作數(shù)量。盡管一些技術(shù)(最顯著的是Carbon 多進(jìn)程服務(wù)—Carbon Multiprocessing Services)使用該術(shù)語的意義有時(shí)不同,但是最通用的用法是表明需要執(zhí)行的工作數(shù)量的抽象概念。
線程(thread)
進(jìn)程里面的一個(gè)執(zhí)行過程流。每個(gè)線程都有它自己的??臻g,但除此之外同一進(jìn)程的其他線程共享內(nèi)存。
定時(shí)源(timer source)
為線程同步事件的源。定時(shí)器產(chǎn)生預(yù)定時(shí)間將要執(zhí)行的一次或重復(fù)事件。
四、結(jié)束語
多線程編程在開發(fā)應(yīng)用的時(shí)候非常有幫助。比如你可以在后臺(tái)加載圖片,等圖片加載完成后再在主線程更新等,或者在后臺(tái)處理一些需要占用CPU很長時(shí)間的事件(比如請(qǐng)求服務(wù)器,加載數(shù)據(jù)等)。要體會(huì)多線程編程的好處,還得多實(shí)戰(zhàn),結(jié)合使用多種多線程技術(shù)。特別要注意Run Loop的使用,很多開發(fā)者在編寫多線程應(yīng)用的時(shí)候很少關(guān)注過Run Loop。如果你仔細(xì)閱讀并掌握Run Loop的細(xì)節(jié),將會(huì)幫助你寫出更優(yōu)美的代碼。同步是多線程編程的老生常談,估計(jì)大學(xué)時(shí)候大家都基本熟悉了同步的重要性。
最后,本文在翻譯過程中發(fā)現(xiàn)很多地方直譯成中文比較晦澀,所以采用了意譯的方式,這不可避免的造成有一些地方可能和原文有一定的出入,所以如果你閱讀的時(shí)候發(fā)現(xiàn)有任何的錯(cuò)誤都可以討論指正。