本文在於說明iOS應用的Objective-C代碼的執行緒安全性。先是簡單介紹一下安全執行緒的基本知識,然後通過一個小例子來觀察非安全執行緒代碼,最後會稍稍介紹一個可以用來分析安全執行緒隱患的工具。
1) 基礎知識 (Threading Basics)
當啟動一個應用時,iOS會對應建立一個進程(process)和一塊為之分配的記憶體。簡單地說,一個應用進程的記憶體包括三個部分: (更詳細的描述可以看 這裡 ):
程式記憶體( program memory)儲存應用的執行代碼,它在執行時由一個指令指標(Instruction Pointer, IP)來跟蹤程式執行位置。
堆( heap )儲存由 [… alloc] init]來建立的對象。
堆棧( stack )則用於函數調用。儲存參數和函數的局部變數。
一個應用進程預設有一個主線程。如果有多線程,所有線程共用 program memory 和 heap , 每個線程又有各自的IP和堆棧。就是說每個線程都有自己的執行流程,當它呼叫一個方法時,其它線程是無法訪問調用參數和該方法的局部變數的。而那些在堆(heap)上建立的對象卻可以被其它線程訪問和使用。
2) 實驗 (Experiment)
建個使用如下代碼的小程式:
[cpp] view plain copy print ? @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:方法中調用它 :
[cpp] view plain copy print ? - (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():
[cpp] view plain copy print ? - (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()