本文在於說明iOS應用的Objective-C代碼的執行緒安全性。先是簡單介紹一下安全執行緒的基本知識,然後通過一個小例子來觀察非安全執行緒代碼,最後會稍稍介紹一個可以用來分析安全執行緒隱患的工具。
1) 基礎知識 (Threading Basics)
當啟動一個應用時,iOS會對應建立一個進程(process)和一塊為之分配的記憶體。簡單地說,一個應用進程的記憶體包括三個部分: (更詳細的描述可以看這裡):
程式記憶體(program memory)儲存應用的執行代碼,它在執行時由一個指令指標(Instruction Pointer, IP)來跟蹤程式執行位置。堆(heap)儲存由[…
alloc] init]來建立的對象。
堆棧(stack)則用於函數調用。儲存參數和函數的局部變數。
一個應用進程預設有一個主線程。如果有多線程,所有線程共用program
memory 和 heap ,
每個線程又有各自的IP和堆棧。就是說每個線程都有自己的執行流程,當它呼叫一個方法時,其它線程是無法訪問調用參數和該方法的局部變數的。而那些在堆(heap)上建立的對象卻可以被其它線程訪問和使用。
2) 實驗 (Experiment)
建個使用如下代碼的小程式:
@interface FooClass {} @property (nonatomic, assign) NSUInteger value; - (void)doIt; @end @implementation FooClass @synthesize value; - (void)doIt { self.value = 0; for (int i = 0; i < 100000; ++i) { self.value = i; } NSLog(@"執行後: %d (%@)", self.value, [NSThread currentThread]); } @end
這個類有一個整型屬性value,並且會在doIt方法被連續增加100000次。執行完後,再將它的值和調用doIt方法的線程資訊輸出出來。 如下在AppDelegate中增加一個_startExperiment方法,然後在application:didFinishLaunchingWithOptions:方法中調用它:
- (void)_startExperiment { FooClass *foo = [[FooClass alloc] init]; [foo doIt]; [foo release]; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // … [self _startExperiment]; return YES; }
因為這裡還有多線程,所以結果很簡單地顯示value值為99999。
3) 安全執行緒 (Thread Safety)
如下以多線程並存執行doIt():
- (void)_startExperiment { FooClass *foo = [[FooClass alloc] init]; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); for (int i = 0; i < 4; ++i) { //四個線程 dispatch_async(queue, ^{ [foo doIt]; }); } [foo release]; }
再執行,你的輸出可能會類似如下的結果:(實際可能不一樣):
after execution: 19851 (NSThread: 0x6b29bd0>{name = (null), num = 3}) after execution: 91396 (NSThread: 0x6b298f0>{name = (null), num = 4}) after execution: 99999 (NSThread: 0x6a288a0>{name = (null), num = 5}) after execution: 99999 (NSThread: 0x6b2a6f0>{name = (null), num = 6})
並不是每個線程的value都是99999。這是因為現在的代碼並不是安全執行緒的。
所謂安全執行緒就是代碼運行在多線程環境下和運行在單線程環境下是一樣的。
是什麼導致了這個行為呢? 正如前面所說的每個線程都有其自己的IP和堆棧,但卻共用堆(heap)。例子中的FooClass是建立在堆上的,所有線程都可以使用。展示了兩個線程在執行doIt方法時的衝突::Thread 1和Thread 2正在不同的位置執行。doIt()並沒有對多線程的執行進行保護,它的實現是非安全執行緒的。一個將doIt()變為安全執行緒的方式是在其函數體外使用如下編譯指示符(directive):@synchronized新的代碼如下所示:
- (void)doIt { @synchronized(self) { self.value = 0; for (int i = 0; i < 100000; ++i) { self.value = i; } NSLog(@"after execution: %d (%@)", self.value, [NSThread currentThread]); } }
使用@synchronized指示符,
每個線程會在doIt()互斥地使用self。不過因為目前的代碼中@synchronized包住了整個函數體,並不能達到並存執行的效果。
另一種同步訪問機制是使用GCD:Grand
Central Dispatch (GCD).
4) 如何識別非安全執行緒的代碼 (How to identify not thread safe code)
上面例子太過於簡單了。現實中,花了時間寫好的代碼,常常遇到死結、崩潰,或者一些無法複現的問題。總之和期望的行為不一樣。
線程問題的主因是共用或全域狀態(state)資料。多個對象訪問一個全域變數,或者在堆中分享了共同對象,再或者向共同的儲存空間寫入資料。在前面例子中所共用的狀態是self, 對應的訪問也就是self.value。例子中所展示要比實際上的情況簡單太多了,事實上確定使用的共用或全域狀態(share
or global state)並不容易。
解決方案就是寫了一個工具,由多線程調用的函數來識別。下面是這個工具的核心概念。
工具主要包含了四個類: MultiThreadingAnalysis的執行個體用於記錄一個線程對方法的調用, ThreadingTrace類和MethodExecution類用來輸出MultiThreadingAnalysis整理的分析結果, MultiThreadingAnalysisHook類則用於hook到對象並追蹤它被調用的所有方法。
MultiThreadingAnalysis類提供兩個方法:
- recordCallToMethod:ofClass:onThread: 記錄某個方法在某個線程上被調用了。
- threadingTraceOfLastApplicationRun 需要在分析完成後調用。
@interface MultiThreadingAnalysis : NSObject - (void)recordCallToMethod:(NSString*)methodName ofClass:(NSString*)className onThread:(NSString*)threadID; - (ThreadingTrace*) threadingTraceOfLastApplicationRun; @end
分析結果由ThreadingTrace來處理.
它包含了一組MethodExecution執行個體,每一個都表示了一個線程對一個方法的調用:
/* * An instance of this class captures * which methods of which classes have been * called on which threads. */ @interface ThreadingTrace : NSObject /* * Set of MethodExecution */ @property (nonatomic, readonly) NSSet *methodExecutions; - (void)addMethodExecution:(MethodExecution*)methodExec; @end /* * An instance of this class represents a call * to a method of a specific class on a thread * with a specific threadID. */ @interface MethodExecution : NSObject @property (nonatomic, retain) NSString *methodName; @property (nonatomic, retain) NSString *className; @property (nonatomic, retain) NSString *threadID; @end
為了儘可能方法地記錄方法的調用,我使用了NSProxy來hook對一個對象所有方法的調用。MultiThreadingAnalysisHook類繼承自NSProxy,並在forwardInvocation: 方法解析對target對象的調用.
在重定位到target對象前,會先使用一個MultiThreadingAnalysis執行個體來記錄下這次調用。
@interface MultiThreadingAnalysisHook : NSProxy @property (nonatomic, retain) id target; @property (nonatomic, retain) MultiThreadingAnalysis *analysis; @end @implementation MultiThreadingAnalysisHook -(void)forwardInvocation:(NSInvocation*)anInvocation { [self.analysis recordCallToMethod:NSStringFromSelector([anInvocation selector]) ofClass:NSStringFromClass([self.target class]) onThread:[NSString stringWithFormat:@"%d", [NSThread currentThread]]]; [anInvocation invokeWithTarget:self.target]; } @end
現在就可以使用了。在你要分析的類中建立一個私人方法_withThreadingAnalysis 。 這個方法要建立一個MultiThreadingAnalysisHook執行個體並且將target指到self。在自行指定的初始化函數中調用_withThreadingAnalysis並返回其結果(HOOK的動作)。這樣就達到使用MultiThreadingAnalysisHook執行個體將原本對象的self封裝起來,並可以記錄所有外部對象的調用。
@implementation YourClass - (id)init { //... do init stuff here return [self _withThreadingAnalysis]; } - (id)_withThreadingAnalysis { MultiThreadingAnalysisHook *hook = [[MultiThreadingAnalysisHook alloc] init]; hook.target = self; return hook; } @end
此後就可以調用MultiThreadingAnalysis
的threadingTraceOfLastApplicationRun方法擷取分析結果。最簡單地輸出到文字檔,結果如下:
begin threading analysis for class FooClass method doIt (_MultiThreadAccess_) method init (_SingleThreadAccess_)
如果某個方法被多線程調用(標註為 _MultiThreadAccess_), 你可以看到更多詳細資料。
原文地址:http://sodecon.blogspot.com/2012/08/ios-multithreading-thread-safety-in-ios.html
轉載請註明出處: http://blog.csdn.net/horkychen