[譯]線程編程指南(三)

由于文章長度限制,本文作為[譯]線程編程指南(二)后續部分。

線程安全技巧

同步工具是保證代碼線程安全的有效方式,但它不是萬能藥。使用太多鎖或者其他類型的同步原語實際上會導致應用多線程的性能反而不如非多線程時的性能。找到安全與性能之間的平衡點是一門需要經驗的藝術。下列章節將為你的應用選擇合適的同步等級提供幫助建議。

避免同步

對于你工作的任何新項目,甚至對現有的項目,設計代碼和數據結構來避免同步使用可能是最好的解決方案。雖然鎖和其他同步工具都很有用,但它們確實會影響任何應用程序的性能。如果總體設計會導致特定資源之間的高度競爭,你的線程甚至會等待更長的時間。

實現并發的最佳方法是減少并發任務之間的交互和相互依賴關系。如果每個任務都在它自己的私有數據集上運行,則不需要使用鎖來保護數據。即使在兩個任務共享一個共同的數據集的情況下,你也可以為每個任務提供自己的備份。當然,復制數據集也有它的成本,所以你在作出決定之前必須權衡這些成本和同步的成本。

理解同步的局限性

同步工具只有在使用多線程的應用中才會有效。如果你創建了一個互斥鎖來限制某個特定資源的訪問,所有的線程必須在嘗試操作該資源前請求這個鎖。如果不這樣做,提供這樣的互斥會另人困惑并成為程序猿的錯誤。

注意代碼正確性

當使用鎖技術和內存屏障技術時,你總是應該更加小心地在代碼中為其提供位置。即使鎖看起來實際上可以讓你產生一種虛假的安全感。下面的例子將會說明這個問題,并指出在看似無害的代碼中的缺陷。基本的前提是,你有一個可變數組包含一組不變的對象。假設你想調用數組中的第一個對象的方法。你可以使用下面的代碼:

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;

[arrayLock lock];

anObject = [myArray objectAtIndex:0];

[arrayLock unlock];

[anObject doSomething];

由于數組是可變的,保護數組的鎖阻止了其他線程對于數組的修改直到你完成了從數組中獲取到想要的對象。同時因為你獲取到的對象是不可變的,所以鎖就沒有必要對調用doSomething方法部分的代碼進行保護。

盡管在前面的例子中存在這樣一個問題。如果釋放鎖時另一個線程來移除數組中的所有對象,你有機會在這之前執行doSomething方法?在一個沒有垃圾收集機制的應用中,代碼中持有的對象可能被釋放,留下一個指向無效內存地址的指針。要解決這個問題,你可以簡單地重新安排你的現有代碼并在調用doSomething后釋放鎖,如下所示:

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;

[arrayLock lock];

anObject = [myArray objectAtIndex:0];

[anObject doSomething];

[arrayLock unlock];

通過在鎖內部調用doSomething方法,代碼可以保證方法調用時對象仍然有效。不幸的是,如果doSomething方法需要花費很長時間來執行,這將導致代碼長時間的持有鎖,并造成性能上的瓶頸。

這段代碼的問題不是臨界區定義得不好,而真正的問題并沒有理解。真正的問題是由其他線程的存在而觸發的內存管理問題。由于對象能夠被其他線程釋放,所以更好的解決辦法是在鎖釋放之前持有anObject。該解決方案解決了對象被釋放的實際問題,并沒有引入一個潛在的性能隱患。

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;

[arrayLock lock];

anObject = [myArray objectAtIndex:0];
[anObject retain];

[arrayLock unlock];

[anObject doSomething];
[anObject release];

盡管先前的示例事實上非常簡單,它們確實說明了非常重要的一點。說到正確性,你必須考慮到這個明顯的問題。內存管理和其他方面的設計也可能會受到多線程存在的影響,所以你要提前考慮這些問題。此外,當涉及到安全問題時,你應該經常假設編譯器可能會做最壞的事情。這種意識和警惕性應該幫助你避免潛在的問題,確保代碼的行為正確。

獲取更多線程安全的示例,請看之后的線程安全總結

當心死鎖與活鎖

任何時候一個線程試圖同時持有一個以上的鎖時,有可能發生死鎖。死鎖發生在兩個不同的線程各自持有一個鎖,而線程同時需要持有對方所持有的鎖時。其結果是,每個線程都會永久地被阻塞,因為它永遠無法獲得另一個鎖。

活鎖和死鎖類似,同樣發生在兩個線程競爭同一資源時。在活鎖的情況中,一個線程放棄其鎖并試圖獲取另一個鎖。一旦它獲得了另一個鎖,它又返回并試圖獲得第一個鎖。這樣一來它會被鎖住,因為它花費了所有時間來釋放一個鎖并試圖獲得另一個鎖,而不是做任何實際工作。

為了同時避免死鎖和活鎖的情況,最好的辦法是一次只拿一個鎖。如果你必須同時獲得一個以上的鎖,你應該確保其他線程不嘗試做相同的事情。

正確使用volatile變量

如果你已經使用互斥鎖來保護一段代碼,不要想當然地認為你需要使用volatile關鍵字來保護該區域的重要變量。互斥包括了一個確保已載入和已存儲操作正確順序的內存屏障。在臨界區內將變量強制設置為volatile可以保證每次獲取的值都是來自于內存當中。在特定情況下這兩種技術結合使用也許是必要的,但也導致顯著的性能損失。如果單獨使用互斥足以保護變量,請省略使用關鍵字volatile。

同樣重要的是,在不使用互斥的時候也不必使用volatile變量。總的來說,互斥和其他同步機制是比volatile更好的保護數據結構完整性的方式。Volatile關鍵字只確保一個變量是從內存中而不是寄存器中加載,并不能確保你的代碼可以正確地訪問該變量。

使用原子操作

非阻塞式的同步是一種可以執行某些操作并避免鎖消耗的方式。雖然鎖是兩個線程間一種有效的同步方式,但請求鎖是資源消耗相對昂貴的操作,即便是在非沖突情況下。相反的,許多原子性操作只占用小部分時間來完成和鎖同樣有效的操作。

原子操作讓你在32位或64位值上執行簡單的數學和邏輯運算。這些操作依賴于特殊的硬件指令(和可選的內存屏障),以確保在相關的內存再次訪問之前完成既定操作。在多線程的情況下,你應該經常使用包含內存障礙的原子操作以確保這部分存儲在線程之間是正確同步的。

表4-3列舉了可用的原子性數學和邏輯操作以及相應的函數名稱。這些函數全部聲明在/usr/include/libkern/OSAtomic.h頭文件中,你可以在里面找到完整的語法。這些函數的64位版本只存在與64位的進程中。

表4-3 原子性的數學和邏輯操作

操作 函數名稱 描述
加(Add) OSAtomicAdd32
OSAtomicAdd32Barrier
OSAtomicAdd64
OSAtomicAdd64Barrier
兩個整型值相加并將結果賦值給指定變量。
遞增(Increment) OSAtomicIncrement32
OSAtomicIncrement32Barrier
OSAtomicIncrement64
OSAtomicIncrement64Barrier
指定整型值加1。
遞減(Decrement) OSAtomicDecrement32
OSAtomicDecrement32Barrier
OSAtomicDecrement64
OSAtomicDecrement64Barrier
指定整型值減1。
邏輯或(Logical OR) OSAtomicOr32
OSAtomicOr32Barrier
在32位值和32位掩碼間執行邏輯或操作。
邏輯與(Logical AND) OSAtomicAnd32
OSAtomicAnd32Barrier
在32位值和32位掩碼間執行邏輯與操作。
邏輯異或(Logical XOR) OSAtomicXor32
OSAtomicXor32Barrier
在32位值和32位掩碼間執行邏輯異或操作。
比較和交換(Compare and swap) OSAtomicCompareAndSwap32
OSAtomicCompareAndSwap32Barrier
OSAtomicCompareAndSwap64
OSAtomicCompareAndSwap64Barrier
OSAtomicCompareAndSwapPtr
OSAtomicCompareAndSwapPtrBarrier
OSAtomicCompareAndSwapInt
OSAtomicCompareAndSwapIntBarrier
OSAtomicCompareAndSwapLong
OSAtomicCompareAndSwapLongBarrier
對變量的舊值進行比較。如果兩個值是相等的,這個函數將指定新值賦給該變量;否則,它什么也不做。比較和賦值作為一個原子操作,該函數會返回一個布爾值以表示是否發生交換。
測試和設置(Test and set) OSAtomicTestAndSet
OSAtomicTestAndSetBarrier
在指定的變量中測試一個位,將該位設置為1,并將老位的值作為布爾值返回。位根據公式進行測試(0x80 >> (n & 7))字節((char*)address + (n >> 3)),n是位號碼和地址是一個指針變量。這個公式有效地分解成8位大小的塊,并在每一個塊中的位順序反轉。例如,為了測試一個32位整數的最低序位(位0),你將實際指定的位號為7;同樣,要測試的最高點位(位32),你將指定24位數字。
測試和清理(Test and clear) OSAtomicTestAndClear
OSAtomicTestAndClearBarrier
在指定的變量中測試一個位,將該位設置為0,并將老位的值返回布爾值。位根據公式進行測試(0x80 >> (n & 7))字節((char*)address + (n >> 3)),n是位號碼和地址是一個指針變量。這個公式有效地分解成8位大小的塊,并在每一個塊中的位順序反轉。例如,為了測試一個32位整數的最低序位(位0),你將實際指定的位號為7;同樣,要測試的最高點位(位32),你將指定24位數字。

大多數原子函數的行為應該是相對簡單并如你所期望的。然而代碼4-1,顯示了原子性的test-and-set以及compare-and-swap操作相對復雜的行為。前面三個調用OSAtomicTestAndSet函數來展示位操作公式如何被用于整型值,并且其結果可能與你所期望的不同。后面兩個調用展示了OSAtomicCompareAndSwap32函數的行為。在所有情況下,這些函數都是在沒有其他線程操作的值的無沖突情況下調用。

代碼4-1 執行原子操作

int32_t  theValue = 0;
OSAtomicTestAndSet(0, &theValue);
// theValue is now 128.

theValue = 0;
OSAtomicTestAndSet(7, &theValue);
// theValue is now 1.

theValue = 0;
OSAtomicTestAndSet(15, &theValue)
// theValue is now 256.

OSAtomicCompareAndSwap32(256, 512, &theValue);
// theValue is now 512.

OSAtomicCompareAndSwap32(256, 1024, &theValue);
// theValue is still 512.

更多有關原子性操作的信息,請查看atomic的man幫助頁或者/usr/include/libkern/OSAtomic.h頭文件。

使用鎖

鎖作為線程編程的基本同步工具。鎖使你能夠很容易地保護大段代碼,這樣你就可以確保代碼的正確性。OS X和iOS為所有應用類型提供了基本的互斥鎖,并且Foundation Framework為特殊的情形定義了額外的變量。下面的章節將向你展示如何使用這些鎖類型。

使用POSIX的Mutex鎖

POSIX的互斥鎖在任何應用中都能夠極其簡單地使用。為創建互斥鎖,你需要聲明并初始化一個pthread_mutex_t結構體。為完成解鎖的操作,你需要使用pthread_mutex_lockpthread_mutex_unlock函數。代碼4-2展示了使用POSIX線程互斥鎖所需要初始化的基本代碼。當你完成了該鎖的操作時,簡單地調用pthread_mutex_destroy函數來釋放鎖。

代碼4-2 使用互斥鎖

pthread_mutex_t mutex;

void MyInitFunction()
{
    pthread_mutex_init(&mutex, NULL);
}

void MyLockingFunction()
{
    pthread_mutex_lock(&mutex);

    // Do work.

    pthread_mutex_unlock(&mutex);
}

注意:以上代碼只是一個展示POSIX線程互斥鎖函數的簡單示例。你自己的代碼必須檢查這些函數返回的錯誤碼并正確地處理它們。

使用NSLock

NSLock對象為Cocoa應用實現了基本的互斥功能。所有鎖(包括NSLock)事實上由NSLocking協議定義,該協議同樣定義了lockunlock方法。你可以在任何需要互斥的地方使用這些方法來請求鎖以及釋放鎖。

除了標準的鎖操作之外,NSLock類還加入了tryLocklockBeforeDate:方法。tryLock方法試圖獲取鎖但在所不可用時并不阻塞線程,而是返回NO。lockBeforeDate:方法在指定時間內鎖不能獲取時試圖獲取鎖但不阻塞線程(并返回NO)。

下面的示例將向你展示如何使用NSLock來調節可視化視圖的更新,視圖更新的數據來自于其他線程的計算結果。如果線程不能立即請求到鎖,它會繼續其計算操作直到所能夠獲取時更新顯示。

BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];

...

while (moreToDo) {
    /* Do another increment of calculation */
    
    /* until there’s no more to do. */
    
    if ([theLock tryLock]) {
        /* Update display used by all threads. */
        [theLock unlock];
    }
}
使用@synchronized

@synchronized語句是Objective-C代碼中創建互斥鎖的便捷方式。@synchronized語句完成其他互斥鎖應該做的事情-它防止不同線程在同一時間請求相同的鎖。在這種情況下,你沒有必要創建互斥鎖或者直接鎖住一個對象。相反地,你可以簡單地使用任何Objective-C對象作為鎖令牌,正如下面代碼所示:

- (void)myMethod:(id)anObj
{
    @synchronized(anObj)
    {
        // Everything between the braces is protected by the @synchronized directive.
    }
}

傳遞給@synchronized語句的對象會成為區別受保護代碼塊的唯一標識。如果你在兩個線程中執行先前的這個方法,并向每個線程傳遞不同的對象作為anObj參數,每個線程會獲得這個鎖并不受阻塞的繼續執行。如果你同時傳遞同一個對象,其中一個線程會首先獲得鎖并使得另一個線程阻塞直到第一個線程退出了臨界區。

作為一種預防措施,@synchronized塊會向受到保護的代碼隱式地添加異常處理回調。該回調在異常拋出時會自動釋放互斥鎖。這意味著為了使用@synchronized語句,你必須在代碼中開啟Objective-C的異常處理。如果你不希望由隱式異常處理程序引起額外的開銷,你應該考慮使用鎖類。

使用其他的Cocoa鎖

下面的章節將描述Cocoa其他類型鎖的使用。

使用NSRecursiveLock對象

NSRecursiveLock類定義了一種可以多次被同一線程請求且不導致線程死鎖的鎖類型。遞歸鎖必須記錄有好多次被成功請求。每次鎖的成功請求必須由相應的次數的解鎖調用來平衡。只有當所有的解鎖調用平衡時鎖才會被釋放并繼續有其他線程請求。

正如其名字暗示的一樣,該類型的鎖通常用于遞歸函數來防止遞歸操作導致線程的阻塞。在非遞歸的情況下,你可以用它來調用那些語意上仍希望持有鎖的函數。下面是一個遞歸函數中請求該鎖的代碼示例。如果你不像代碼中那樣使用NSRecursiveLock對象,你的線程在函數再次調用時產生死鎖。

NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];

void MyRecursiveFunction(int value)
{
    [theLock lock];

    if (value != 0)
    {
        --value;
        MyRecursiveFunction(value);
    }

    [theLock unlock];
}

MyRecursiveFunction(5);

注意:由于遞歸鎖直到全部調用和解鎖調用平衡時才釋放,你應該仔細權衡使用鎖和這樣做造成潛在的性能影響。在一個較長的時間內保持任何鎖會使其它線程阻塞直到遞歸完成。如果可以重寫代碼來消除遞歸或需要使用的遞歸鎖,則可以實現更好的性能。

使用NSConditionLock對象

NSConditionLock類定義了可以根據特殊值來進行解鎖操作的互斥鎖。你不應該將該類型的鎖和之前的條件量混為一談。它和條件量某種意義上講行為相似,但實現方式完全不同。

通常,你將NSConditionLock對象用于線程需要執行特定順序的任務時,比如一個線程生產數據而另一個線程消費數據。當生產者執行時,消費者請求鎖的條件取決于你的程序。(條件本身僅僅是一個定義的整型值)當生產者完成時,它會解鎖并將鎖條件置為合適的整型值來喚醒消費者線程,消費者線程然后收到并處理數據。

NSConditionLock對象中的鎖定和解鎖方法可以任意地組合使用。例如,你可以將鎖定信息配對給unlockWithCondition:,或者解鎖信息配對給lockWithCondition:。當然,這一組合解鎖但不會釋放任何線程等待特定的條件值。

下面的示例演示了如何使用條件鎖處理“生產者-消費者”問題。設想應用程序包含一個數據隊列。生產者線程將數據添加到隊列,而消費者線程從隊列中提取數據。生產者不需要等待一個特定的條件,但它必須等待鎖以便它可以安全地添加數據到隊列。

id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];

while(true)
{
    [condLock lock];

    /* Add data to the queue. */

    [condLock unlockWithCondition:HAS_DATA];
}

因為鎖的初始條件設置為NO_DATA,所以生產者線程期初獲取鎖并不受影響。它將隊列填充好數據并將條件設置為HAS_DATA。在隨后的迭代中,生產者線程可以在到達時添加新的數據不管隊列是否是空的還是有一些數據。當消費者線程從隊列中提取數據時,它阻塞的唯一時間是消費者線程從隊列中提取數據時。

因為消費者線程必須要有數據處理,它根據特定的條件等待隊列。當生產者將數據放在隊列上時,消費者線程喚醒并請求鎖。然后,它可以從隊列中提取一些數據并更新隊列狀態。下面的示例顯示了消費者線程處理循環的基本結構。

while (true)
{
    [condLock lockWhenCondition:HAS_DATA];

    /* Remove data from the queue. */

    [condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)];

    // Process the data locally.
}
使用NSDistributedLock對象

NSDistributedLock類可用于多個宿主機上的多個應用之間來限制某些共享資源的訪問,例如文件。該鎖本身是一種由文件系統(如文件或者目錄)實現的非常高效的互斥鎖。為使NSDistributedLock對象可用,該鎖必須由所有的應用來使用。這通常意味著把它放在所有計算機上的應用程序都可以訪問的文件系統中。

不像其他類型的鎖,NSDistributedLock并不遵循NSLocking協議并且沒有lock方法。lock方法會阻塞線程的執行,并要求系統以一個預定的速率輪詢鎖。NSDistributedLock提供tryLock方法讓你決定是否輪詢,而不是在你自己的代碼中這樣做。

由于它使用文件系統來實現,NSDistributedLock對象直到在持有者顯式地釋放它時釋放。如果你的應用在持有分布式鎖是崩潰了,其他的客戶端將不能對保護資源進行訪問。在這種情況下,你可以使用breakLock方法來打破既存鎖以便你能夠請求到它。破壞鎖通常是需要避免的,除非你確定鎖的持有者死掉了且不能釋放鎖。

同其他類型的鎖一樣,當你用完NSDistributedLock對象后,可以使用unlock方法來釋放它。

使用條件量

條件量是一種用于同步操作順序的特殊類型的鎖。它與互斥鎖之間只有細微的差別。線程會保持阻塞直到其他線程顯式地喚醒條件量。

由于細節涉及到操作系統實現,條件量允許假定還鎖成功,即使沒有在代碼中喚醒它們。為了避免這些虛假信號引起的問題,你應該經常使用一個謂詞與你的條件量一起使用。謂詞是一個更具體的方法,它決定是否安全地為你的線程進行處理。條件量簡單地保持你的線程睡眠,直到謂詞可以被喚醒線程設置。

下面的章節將告訴你如何在代碼中使用條件量。

使用NSCondition

NSCondition類提供了和POSIX條件量語意相同,但同時包裝了鎖和條件數據到單個對象的數據結構。這就使得對象可以像互斥鎖并且像條件量那樣等待條件。

代碼4-3代碼段展示了為等待NSCondition對象的事件隊列。cocoaCondition變量包含一個NSCondition對象和timeToDoWork,由其他線程喚醒條件時自增的整型變量。

代碼4-3 使用Cocoa條件量

[cocoaCondition lock];

while (timeToDoWork <= 0)

    [cocoaCondition wait];

timeToDoWork--;

// Do real work here.
[cocoaCondition unlock];

代碼4-4展示了喚醒Cocoa條件量并完成謂詞變量自增的代碼。你應該總是在喚醒條件量之前鎖住它。

代碼4-4 喚醒Cocoa條件量

[cocoaCondition lock];

timeToDoWork++;

[cocoaCondition signal];

[cocoaCondition unlock];
使用POSIX的條件量

POSIX線程的條件量同時滿足條件數據結構和互斥鎖的功能。盡管兩個鎖結構各自獨立,但在運行時互斥鎖緊密地綁定著條件結構。等待一個信號的線程應該總是一起使用這樣非互斥鎖和條件結構。改變這樣的配對可能造成錯誤。

代碼4-5展示了條件量和謂詞的基本初始化和使用。經過初始化的條件量和互斥鎖,等待線程使用ready_to_go變量作為謂詞并進入while循環。只有當謂詞被設置并且緊接著條件量被發出,等待線程才喚醒并開始做它的工作。

代碼4-5 使用POSIX條件量

pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean     ready_to_go = true;

void MyCondInitFunction()
{
    pthread_mutex_init(&mutex);
    pthread_cond_init(&condition, NULL);
}

void MyWaitOnConditionFunction()
{
    // Lock the mutex.
    pthread_mutex_lock(&mutex);

    // If the predicate is already set, then the while loop is bypassed;
    // otherwise, the thread sleeps until the predicate is set.
    while(ready_to_go == false)
    {
        pthread_cond_wait(&condition, &mutex);
    }

    // Do work. (The mutex should stay locked.)

    // Reset the predicate and release the mutex.
    ready_to_go = false;

    pthread_mutex_unlock(&mutex);
}

發信號線程負責設置謂詞,并將信號發送到條件量。代碼4-6顯示了實現該行為的代碼。在這個例子中,條件量是在互斥內部被喚醒以防止等待條件的線程間的競態條件發生。

代碼4-6 喚醒條件量

void SignalThreadUsingCondition()
{
    // At this point, there should be work for the other thread to do.
    pthread_mutex_lock(&mutex);
    ready_to_go = true;

    // Signal the other thread to begin work.
    pthread_cond_signal(&condition);

    pthread_mutex_unlock(&mutex);
}

注意:以上代碼只是一個展示POSIX線程條件量函數的簡單示例。你自己的代碼必須檢查這些函數返回的錯誤碼并正確地處理它們。

附錄A:線程安全總結

本附錄描述了OS X和iOS中某些關鍵框架的高級別的線程安全。本附錄中的信息是隨時變更的。

Cocoa

多線程中使用Cocoa的指導如下:

  • 不可變(immutable)對象通常是線程安全的。一旦你創建它們,你可以在線程間安全地傳遞這些對象。另一方面,可變(mutable)對象通常不是線程安全的。在多線程應用中使用可變對象,應用程序必須正確同步。
  • 許多對象看似“安全”實則在多線程中使用時不安全。許多這些對象可以在任何線程中使用,只要在同一時間且同一線程。被嚴格限制在應用程序的主線程上的對象被調用時就是如此。
  • 應用程序的主線程負責處理事件。雖然其他線程進入事件路徑時Application Kit將繼續工作,但它的操作可發生在事件隊列之外。
  • 如果你想用線程來繪制視圖,使用NSView的lockFocusIfCanDrawunlockFocus方法將所有繪制代碼包括進來。
  • 為了能在Cocoa中使用POSIX線程,你必須首先將應用置于多線程模式。
Foundation Framework線程安全

有一種誤解,認為Foundation Framework是線程安全的,而Application Kit不是線程安全的。不幸的是,這只是一個總的概括,有些誤導。每個框架都有線程安全的區域和線程不安全的區域。下面的章節描述了Foundation Framework的通用的線程安全性。

線程安全的類和函數

下列的類和函數通常被認為是線程安全的。你可以在多個線程中使用相同實例而不需請求鎖。

NSArray
NSAssertionHandler
NSAttributedString
NSCalendarDate
NSCharacterSet
NSConditionLock
NSConnection
NSData
NSDate
NSDecimal 函數
NSDecimalNumber
NSDecimalNumberHandler
NSDeserializer
NSDictionary
NSDistantObject
NSDistributedLock
NSDistributedNotificationCenter
NSException
NSFileManager (OS X 10.5及后續版本)
NSHost
NSLock
NSLog/NSLogv
NSMethodSignature
NSNotification
NSNotificationCenter
NSNumber
NSObject
NSPortCoder
NSPortMessage
NSPortNameServer
NSProtocolChecker
NSProxy
NSRecursiveLock
NSSet
NSString
NSThread
NSTimer
NSTimeZone
NSUserDefaults
NSValue
NSXMLParser
對象的allocation 和 retain count 函數
Zone 和 memory 函數
非線程安全的類和函數

下列的類和函數通常被認為是非線程安全的。大多數情況下,你可以在多線程環境使用這些類只要你在同一時刻同一線程中。

NSArchiver
NSAutoreleasePool
NSBundle
NSCalendar
NSCoder
NSCountedSet
NSDateFormatter
NSEnumerator
NSFileHandle
NSFormatter
NSHashTable 函數
NSInvocation
NSJavaSetup 函數
NSMapTable 函數
NSMutableArray
NSMutableAttributedString
NSMutableCharacterSet
NSMutableData
NSMutableDictionary
NSMutableSet
NSMutableString
NSNotificationQueue
NSNumberFormatter
NSPipe
NSPort
NSProcessInfo
NSRunLoop
NSScanner
NSSerializer
NSTask
NSUnarchiver
NSUndoManager
User name 和 home directory 函數

請注意,盡管NSSerializerNSArchiverNSCoderNSEnumerator對象自身都是線程安全的,它們被列入這里的原因是當它們包裹的數據對象被修改時是不安全的。比如,在使用歸檔的情況下,改變已歸檔的對象圖是不安全的。對于枚舉器,任何線程修改枚舉集合是不安全的。

只能在主線程中使用的類

以下類必須僅從應用程序的主線程中使用。

NSAppleScript
可變 VS 不可變

不可變對象通常是線程安全的;一旦完成對其創建,你可以在線程間安全地傳遞這些對象。當然,當使用不可變對象時,你仍需要記住引用計數的正確使用。如果你不正確地釋放不想保留的對象,隨后也會造成異常。

可變對象通常是非線程安全的。為在多線程應用中使用可變對象,應用必須使用鎖技術同步地訪問它們。總之,集合類型(如NSMutableArray,NSMutableDictionary)是非線程安全的。也就是說,如果一個或多個線程正在修改同一個數組,你必須在其讀寫區域上鎖以確保線程安全。

即便某一個方法聲明返回一個不可變對象,你絕不應該簡單地假設返回的對象是不可變的。取決于該方法的實現,返回的對象可能是可變的也有可能是不可變的。例如,一個本該返回NSString的方法由于其實現,可能事實上返回了一個NSMutableString。如果你想保證對象是不可變的,則必須創建一個不可變的備份。

可重入

TODO

類的初始化

Objective-C的運行時系統會在類接收其他消息前向其發送initialize消息。這將使類在使用前有機會設置其運行時環境。在多線程應用中,運行時保證只有一個線程-這個線程恰好向類發送第一條消息,即執行initialize方法。如果當第一個線程已經進入了initialize方法而第二個線程試圖向該類放松消息時,第二個線程會阻塞直到initialize方法完成執行。同時,第一個線程可以繼續調用該類的其他方法。initialize方法不應該由第二個線程調用;如果這樣做了,兩個線程會死鎖。

由于OS X 10.1.x及其早期版本存在的一個bug,線程能夠在其他線程執行完initialize方法前向類發送消息。這樣一來線程會訪問到并未完全初始化好的值,并可能使應用崩潰。如果你遇到這樣的問題,你需要引入鎖來阻止值的訪問直到它們完全地被初始化或者在類變成多線程操作前強制類初始化自身。

自動釋放池

每個線程都維護著自己的NSAutoreleasePool對象棧。Cocoa認為當前線程的堆棧中總是有一個可用的自動釋放池。如果一個池不可用,對象不被釋放并導致內存泄漏。在基于Application Kit的應用主線程中,其NSAutoreleasePool對象會自動創建和銷毀,但輔助線程(和僅使用Foundation的應用)在使用Cocoa前必須自己創建。如果你的線程是長期運行的且潛在地生成了大量的自動釋放對象,你應該周期性地銷毀和創建自動釋放池(如Application Kit在主線程中所做一樣);否則,自動釋放對象的積累并導致內存的增長。如果你的分離線程不使用Cocoa,則不需要創建一個自動釋放池。

Run Loops

每個線程有且僅有一個run loop。每個run loop,都有自己的一系列模式來決定哪一個輸入源被監聽。Run loop中定義的模式不受其他run loop模式的影響,即便它們有相同的名稱。

如果你的應用基于Application Kit主線程的run loop將自動運行,但是輔助線程(和僅使用Foundation的應用)必須自己啟動run loop。如果分離不進入run loop,在其方法執行完畢后線程會立即退出。

雖然出于一些外部因素,NSRunLoop類并不是線程安全的,你只應該從持有它的線程中使用該類的實例方法。

Application Kit 框架線程安全

下面部分描述了Application Kit框架中常用的線程安全內容。

非線程安全類

下列的類和函數通常是非線程安全你的。在大多數情況下,你可以在多線程環境下使用它們,僅當同一時刻同一線程時。

  • NSGraphicsContext。
  • NSImage。
  • NSResponder。
  • NSWindow及其所有的子類。
只能在主線程中使用的類

下列的類只能用于應用的主線程中。

  • NSCell及其子類。
  • NSView及其子類。
窗口限制

你可以在輔助線程上創建窗口。Application Kit可以確保與窗口關聯的數據結構在主線程上被銷毀以防止競態情況發生。如果應用程序同時處理大量的窗口,也存在窗口對象內存泄漏的可能。

你可以在輔助線程上創建一個模態的窗口。當主線程運行在run loop的模態模式下時,Application Kit會阻塞輔助線程的調用。

事件處理限制

應用的主線程負責處理事件。主線程被NSApplication的run方法調用時阻塞,在應用的main函數中調用。雖然其他線程進入事件路徑時Application Kit將繼續工作,但它的操作可發生在事件隊列之外。例如,如果兩個線程同時響應一個關鍵事件,事件會被亂序接收。讓主線程處理事件,可以帶來一致性的用戶體驗。一旦收到事件,事件會被分發到輔助線程以供后續處理。

你可以從輔助線程調用NSApplication的postEvent:atStart:方法來向主線程的事件隊列推送事件。然而,由于用戶輸入的事件不同順序并不能夠得到保障。應用的主線程仍會負責處理事件隊列中的事件。

圖形繪制限制

Application Kit中使用圖形相關的類和函數繪圖通常是線程安全的,包括NSBezierPath和NSString類。使用特定類的細節在下面部分將會描述。

  • NSView限制
    TODO
  • NSGraphicsContext限制
    TODO
  • NSImage限制
    TODO
Core Data 框架線程安全

Core Data框架支持多線程,盡管其中有些注意事項。獲取更多相關注意事項,請查看《Core Data Programming Guide》

Core Foundation

Core Foundation足夠的線程安全,如果程序中加以小心,你應該不會陷入任何線程沖突的問題。通常情況下它都是線程安全的,比如說查詢、保留、釋放或者傳遞不可變的對象。即便多個線程對共享的對象進行請求,它都是可靠的線程安全。

類似Cocoa,Core Foundation遭遇對象及對象內部的變化時變得線程不安全的情況。例如,正如你所預期的那樣,修改可變數據或可變數組對象就是非線程安全的,修改不可變數組中的對象時也是如此。出于性能因素考慮,在這些情況下是至關重要的。此外,在該層級通常是不可能實現絕對的線程安全。你不能排除,如保持一個從集合中獲得的對象導致的不確定行為。集合本身可以在調用保留所包含的對象之前被釋放。

在這些情況下,從多線程中訪問Core Foundation對象,你的代碼應該防止以鎖的方式同時訪問。例如,代碼枚舉了一個Core Foundation數組中的對象,應使用適當的鎖定調用該枚舉塊以防止數組被其他線程改變。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容