多线程理解
http://www.objc.io/站點主要以雜志的形式,深入挖掘在OC中的最佳編程實踐和高級技術,每個月探討一個主題,每個主題都會有幾篇相關的文章出爐,2013年7月份的主題是并發編程,今天挑選其中的第2篇文章(Concurrent Programming: APIs and Challenges)進行翻譯,與大家分享一下主要內容。由于內容比較多,我將分兩部分翻譯(API和難點)完成,翻譯中,如有錯誤,還請指正。
目錄
1、介紹
2、OS X和iOS中的并發編程
2.1、Threads
2.2、Grand Central Dispatch
2.3、Operation Queues
2.4、Run Loops
3、并發編程中面臨的挑戰
3.1、資源共享
3.2、互斥
3.3、死鎖
3.4、饑餓
3.5、優先級反轉
4、小結
正文
1、介紹
并發的意思就是同時運行多個任務,這些任務可以在單核CPU上以分時(時間共享)的形式同時運行,或者在多核CPU上以真正的并行來運行多任務。
OS X和iOS提供了幾種不同的API來支持并發編程。每種API都具有不同的功能和一些限制,一般是根據不同的任務使用不同的API。這些API在系統中處于不同的地方。并發編程對于開發者來說非常的強大,但是作為開發者需要擔負很大的責任,來把任務處理好。
實際上,并發編程是一個很有挑戰的主題,它有許多錯綜復雜的問題和陷阱,當開發者在使用類似GCD或NSOperationQueue API時,很容易遺忘這些問題和陷阱。本文首先介紹一下OS X和iOS中不同的并發編程API,然后深入了解并發編程中開發者需要面臨的一些挑戰。
2、OS X和iOS中的并發編程
在移動和桌面操作系統中,蘋果提供了相同的并發編程API。 本文會介紹pthread和NSThread、Grand Central Dispatch(GCD)、NSOperationQueue,以及NSRunLoop。NSRunLoop列在其中,有點奇怪,因為它并沒有被用來實現真正的并發,不過NSRunLoop與并發編程有莫大的關系,值得我們去了解。
由于高層API是基于底層API構建的,所以首先將從底層的API開始介紹,然后逐步介紹高層API,不過在具體編程中,選擇API的順序剛好相反:因為大多數情況下,選擇高層的API不僅可以完成底層API能完成的任務,而且能夠讓并發模型變得簡單。
如果你對這里給出的建議(API的選擇)上有所顧慮,那么你可以看看本文的相關內容:并發編程面臨的挑戰,以及Peter Steinberger寫的關于線程安全的文章。
2.1、THREADS
線程(thread)是組成進程的子單元,操作系統的調度器可以對線程進行單獨的調度。實際上,所有的并發編程API都是構建于線程之上的 包括GCD和操作隊列(operation queues)。
多線程可以在單核CPU上同時運行(可以理解為同一時間) 操作系統將時間片分配給每一個線程,這樣就能夠讓用戶感覺到有多個任務在同時進行。如果CPU是多核的,那么線程就可以真正的以并發方式被執行,所以完成某項操作,需要的總時間更少。
開發者可以通過Instrument中的CPU strategy view來觀察代碼被執行時在多核CPU中的調度情況。
需要重點關注的一件事:開發者無法控制代碼在什么地方以及什么時候被調度,以及無法控制代碼執行多長時間后將被暫停,以便輪到執行別的任務。線程調度是非常強大的一種技術,但是也非常復雜(稍后會看到)。
先把線程調度的復雜情況放一邊,開發者可以使用POSIX線程API,或者Objective-C中提供的對該API的封裝 NSThread,來創建自己的線程。下面這個小示例是利用pthread來查找在一百萬個數字中的最小值和最大值。其中并發執行了4個線程。從該示例復雜的代碼中,可以看出為什么我們不希望直接使用pthread。
struct threadInfo {
uint32_t * inputValues;
size_t count;
};
struct threadResult {
uint32_t min;
uint32_t max;
};
void * findMinAndMax(void *arg)
{
struct threadInfo const * const info = (struct threadInfo *) arg;
uint32_t min = UINT32_MAX;
uint32_t max = 0;
for (size_t i = 0; i < info->count; ++i) {
uint32_t v = info->inputValues[i];
min = MIN(min, v);
max = MAX(max, v);
}
free(arg);
struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));
result->min = min;
result->max = max;
return result;
}
int main(int argc, const char * argv[])
{
size_t const count = 1000000;
uint32_t inputValues[count];
// Fill input values with random numbers:
for (size_t i = 0; i < count; ++i) {
inputValues[i] = arc4random();
}
// Spawn 4 threads to find the minimum and maximum:
size_t const threadCount = 4;
pthread_t tid[threadCount];
for (size_t i = 0; i < threadCount; ++i) { struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info)); size_t offset = (count / threadCount) * i; info->inputValues = inputValues + offset;
info->count = MIN(count - offset, count / threadCount);
int err = pthread_create(tid + i, NULL, findMinAndMax, info);
NSCAssert(err == 0, @"pthread_create() failed: %d", err);
}
// Wait for the threads to exit:
struct threadResult * results[threadCount];
for (size_t i = 0; i < threadCount; ++i) {
int err = pthread_join(tid[i], (void **) (results[i]));
NSCAssert(err == 0, @"pthread_join() failed: %d", err);
}
// Find the min and max:
uint32_t min = UINT32_MAX;
uint32_t max = 0;
for (size_t i = 0; i < threadCount; ++i) { min = MIN(min, results[i]->min);
max = MAX(max, results[i]->max);
free(results[i]);
results[i] = NULL;
}
NSLog(@"min = %u", min);
NSLog(@"max = %u", max);
NSThread是Objective-C對pthread的一個封裝。通過封裝,在Cocoa環境中,可以讓代碼看起來更加親切。例如,開發者可以利用NSThread的一個子類來定義一個線程,在這個子類的中封裝了需要運行的代碼。針對上面的那個例子,我們可以定義一個這樣的NSThread子類:
@interface FindMinMaxThread : NSThread
@property (nonatomic) NSUInteger min;
@property (nonatomic) NSUInteger max;
- (instancetype)initWithNumbers:(NSArray *)numbers;
@end
@implementation FindMinMaxThread {
NSArray *_numbers;
}
- (instancetype)initWithNumbers:(NSArray *)numbers
{
self = [super init];
if (self) {
_numbers = numbers;
}
return self;
}
- (void)main
{
NSUInteger min;
NSUInteger max;
// process the data
self.min = min;
self.max = max;
}
@end
要想啟動一個新的線程,需要創建一個線程對象,然后調用它的start方法:
NSSet *threads = [NSMutableSet set];
NSUInteger numberCount = self.numbers.count;
NSUInteger threadCount = 4;
for (NSUInteger i = 0; i < threadCount; i++) {
NSUInteger offset = (count / threadCount) * i;
NSUInteger count = MIN(numberCount - offset, numberCount / threadCount);
NSRange range = NSMakeRange(offset, count);
NSArray *subset = [self.numbers subarrayWithRange:range];
FindMinMaxThread *thread = [[FindMinMaxThread alloc] initWithNumbers:subset];
[threads addObject:thread];
[thread start];
}
現在,當4個線程結束的時候,我們檢測到線程的isFinished屬性。不過最好還是遠離上面的代碼吧 最主要的原因是,在編程中,直接使用線程(無論是pthread,還是NSThread)都是難以接受的。
使用線程會引發的一個問題就是:在開發者自己的代碼,或者系統內部的框架代碼中,被激活的線程數量很有可能會成倍的增加 這對于一個大型工程來說,是很常見的。例如,在8核CPU中,你創建了8個線程,然后在這些線程中調用了框架代碼,這些代碼也創建了同樣的線程(其實它并不知道你已經創建好線程了),這樣會很快產生成千上萬個線程,最終導致你的程序被終止執行 線程實際上并不是免費的咖啡,每個線程的創建都會消耗一些內容,以及相關的內核資源。
下面,我將介紹兩個基于隊列的并發編程API:GCD和operation queue。它們通過集中管理一個線程池(被沒一個任務協同使用),來解決上面遇到的問題。
2.2、Grand Central Dispatch
為了讓開發者更加容易的使用設備上的多核CPU,蘋果在OS X和iOS 4中引入了Grand Central Dispatch(GCD)。在下一篇文章中會更加詳細的介紹GCD:low-level concurrency APIs。
通過GCD,開發者不用再直接跟線程打交道了,只需要向隊列中添加block代碼即可,GCD在后端管理著一個線程池。GCD不僅決定著哪個線程(block)將被執行,它還根據可用的系統資源對線程池中的線程進行管理 這樣可以不通過開發者來集中管理線程,緩解大量線程的創建,做到了讓開發者遠離線程的管理。
默認情況下,GCD公開有5個不同的隊列:運行在主線程中的main queue,3個不同優先級的后臺隊列,以及一個優先級更低的后臺隊列(用于I/O)。另外,開發者可以創建自定義隊列:串行或者并行隊列。自定義隊列非常強大,在自定義隊列中被調度的所有block都將被放入到系統的線程池的一個全局隊列中。
這里隊列中,可以使用不同優先級,這聽起來可能非常簡單,不過,強烈建議,在大多數情況下使用默認的優先級就可以了。在隊列中調度具有不同優先級的任務時,如果這些任務需要訪問一些共享的資源,可能會迅速引起不可預料到的行為,這樣可能會引起程序的突然停止 運行時,低優先級的任務阻塞了高優先級任務。更多相關內容,在本文的優先級反轉中會有介紹。
雖然GCD是稍微偏底層的一個API,但是使用起來非常的簡單。不過這也容易使開發者忘記并發編程中的許多注意事項和陷阱。讀者可以閱讀本文后面的:并發編程中面臨的挑戰,這樣可以注意到一些潛在的問題。本期的另外一篇文章:Low-level Concurrency API,給出了更加深入的解釋,以及一些有價值的提示。
2.3、OPERATION QUEUES
操作隊列(operation queue)是基于GCD封裝的一個隊列模型。GCD提供了更加底層的控制,而操作隊列在GCD之上實現了一些方便的功能,這些功能對于開發者來說會更好、更安全。
類NSOperationQueue有兩個不同類型的隊列:主隊列和自定義隊列。主隊列運行在主線程之上,而自定義隊列在后臺執行。任何情況下,在這兩種隊列中運行的任務,都是由NSOperation組成。
定義自己的操作有兩種方式:重寫main或者start方法,前一種方法非常簡單,但是靈活性不如后一種。對于重寫main方法來說,開發者不需要管理一些狀態屬性(例如isExecuting和isFinished) 當main返回的時候,就可以假定操作結束。
@implementation YourOperation
- (void)main
{
// do your work here ...
}
@end
如果你希望擁有更多的控制權,以及在一個操作中可以執行異步任務,那么就重寫start方法:
@implementation YourOperation
- (void)start
{
self.isExecuting = YES;
self.isFinished = NO;
// start your work, which calls finished once it's done ...
}
- (void)finished
{
self.isExecuting = NO;
self.isFinished = YES;
}
@end
注意:這種情況下,需要開發者手動管理操作的狀態。 為了讓操作隊列能夠捕獲到操作的改變,需要將狀態屬性以KVO的方式實現。并確保狀態改變的時候發送了KVO消息。
為了滿足操作隊列提供的取消功能,還應該檢查isCancelled屬性,以判斷是否繼續運行。
- (void)main
{
while (notDone !self.isCancelled) {
// do your processing
}
}
當開發者定義好操作類之后,就可以很容易的將一個操作添加到隊列中:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
YourOperation *operation = [[YourOperation alloc] init];
[queue addOperation:operation];
另外,開發者也可以將block添加到隊列中。這非常的方便,例如,你希望在主隊列中調度一個一次性任務:
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// do something...
}];
如果重寫operation的description方法,可以很容易的標示出在某個隊列中當前被調度的所有operation。
除了提供基本的調度操作或block外,操作隊列還提供了一些正確使用GCD的功能。例如,可以通過maxConcurrentOperationCount屬性來控制一個隊列中可以有多少個操作參與并發執行,以及將隊列設置為一個串行隊列。
另外還有一個方便的功能就是根據隊列中operation的優先級對其進行排序,這不同于GCD的隊列優先級,它只會影響到一個隊列中所有被調度的operation的執行順序。如果你需要進一步控制operation的執行順序(除了使用5個標準的優先級),還可以在operation之間指定依賴,如下:
[intermediateOperation addDependency:operation1];
[intermediateOperation addDependency:operation2];
[finishedOperation addDependency:intermediateOperation];
上面的代碼可以確保operation1和operation在intermediateOperation之前執行,也就是說,在finishOperation之前被執行。對于需要明確的執行順序時,操作依賴是非常強大的一個機制。 它可以讓你創建一些操作組,并確保這些操作組在所依賴的操作之前被執行,或者在并發隊列中以串行的方式執行operation。
從本質上來看,操作隊列的性能比GCD要低,不過,大多數情況下,可以忽略不計,所以操作隊列是并發編程的首選API。
2.4、RUN LOOPS
實際上,Run loop并不是一項并發機制(例如GCD或操作隊列),因為它并不能并行執行任務。不過在主dispatch/operation隊列中,run loop直接配合著任務的執行,它提供了讓代碼異步執行的一種機制。
Run loop比起操作隊列或者GCD來說,更加容易使用,因為通過run loop,開發者不必處理并發中的復雜情況,就能異步的執行任務。
一個run loop總是綁定到某個特定的線程中。main run loop是與主線程相關的,在每一個Cocoa和CocoaTouch程序中,這個main run loop起到核心作用 它負責處理UI時間、計時器,以及其它內核相關事件。無論什么時候使用計時器、NSURLConnection或者調用performSelector:withObject:afterDelay:,run loop都將在后臺發揮重要作用 異步任務的執行。
無論什么時候,依賴于run loop使用一個方法,都需要記住一點:run loop可以運行在不同的模式中,每種模式都定義了一組事件,供run loop做出響應 這其實是非常聰明的一種做法:在main run loop中臨時處理某些任務。
在iOS中非常典型的一個示例就是滾動,在進行滾動時,run loop并不是運行在默認模式中的,因此,run loop此時并不會做出別的響應,例如,滾動之前在調度一個計時器。一旦滾動停止了,run loop會回到默認模式,并執行添加到隊列中的相關事件。如果在滾動時,希望計時器能被觸發,需要將其在NSRunLoopCommonModes模式下添加到run loop中。
其實,默認情況下,主線程中總是有一個run loop在運行著,而其它的線程默認情況下,不會有run loop。開發者可以自行為其它的線程添加run loop,只不過很少需要這樣做。大多數時候,使用main run loop更加方便。如果有大量的任務不希望在主線程中執行,你可以將其派發到別的隊列中。相關內容,Chris寫了一篇文章,可以去看看:common background practices。
如果你真需要在別的線程中添加一個run loop,那么不要忘記在run loop中至少添加一個input source。如果run loop中沒有input source,那么每次運行這個run loop,都會立即退出。
關于并發編程中面臨的挑戰,會在下一篇文章中出現。
?
轉載于:https://www.cnblogs.com/android-dev/p/3769564.html
總結
- 上一篇: 写个三个月后的我
- 下一篇: 进阶中级程序员需要做的事