線程與GCD

OC在中國網址:http://www.objccn.io/issue-2-1/#challenges

GCDblog:http://www.cnblogs.com/pure/archive/2013/03/31/2977420.html


一、線程(thread)是組成進程的子單元,操作系統的調度器可以對線程進行單獨的調度。實際上,所有的併發編程 API 都是構建於線程之上的 —— 包括 GCD 和操作隊列(operation queues)。NSThread 是 Objective-C 對 pthread 的一個封裝,不論使用 pthread 還是 NSThread 來直接對線程操作,都是相對糟糕的編程體驗,這種方式並不適合我們以寫出良好代碼爲目標的編碼精神。直接使用線程可能會引發的一個問題是,如果你的代碼和所基於的框架代碼都創建自己的線程時,那麼活動的線程數量有可能以指數級增長。這在大型工程中是一個常見問題。例如,在 8 核 CPU 中,你創建了 8 個線程來完全發揮 CPU 性能。然而在這些線程中你的代碼所調用的框架代碼也做了同樣事情(因爲它並不知道你已經創建的這些線程),這樣會很快產生成成百上千的線程。代碼的每個部分自身都沒有問題,然而最後卻還是導致了問題。使用線程並不是沒有代價的,每個線程都會消耗一些內存和內核資源。

二、Grand Centeral Dispatch

通過 GCD,開發者不用再直接跟線程打交道了,只需要向隊列中添加代碼塊即可,GCD 在後端管理着一個線程池

GCD公開有5個不同的隊列:運行在主線程中的main queue,3個不同優先級的後臺隊列,以及一個優先級更低的後臺隊列(用於I/O).

三、Operation Queues

操作隊列(operation queue)是由 GCD 提供的一個隊列模型的 Cocoa 抽象。GCD 提供了更加底層的控制,而操作隊列則在 GCD 之上實現了一些方便的功能,這些功能對於 app 的開發者來說通常是最好最安全的選擇。

NSOperationQueue 有兩種不同類型的隊列:主隊列和自定義隊列。主隊列運行在主線程之上,而自定義隊列在後臺執行。在兩種類型中,這些隊列所處理的任務都使用 NSOperation 的子類來表述。你可以通過重寫 main 或者 start 方法 來定義自己的 operations 。前一種方法非常簡單,開發者不需要管理一些狀態屬性(例如 isExecutingisFinished),當 main 方法返回的時候,這個 operation 就結束了。這種方式使用起來非常簡單,但是靈活性相對重寫 start 來說要少一些。如果你希望擁有更多的控制權,以及在一個操作中可以執行異步任務,那麼就重寫 start 方法:注意:這種情況下,你必須手動管理操作的狀態。 爲了讓操作隊列能夠捕獲到操作的改變,需要將狀態的屬性以配合 KVO 的方式進行實現。如果你不使用它們默認的 setter 來進行設置的話,你就需要在合適的時候發送合適的 KVO 消息。爲了能使用操作隊列所提供的取消功能,你需要在長時間操作中時不時地檢查 isCancelled 屬性。

除了提供基本的調度操作或 block 外,操作隊列還提供了在 GCD 中不太容易處理好的特性的功能。例如,你可以通過 maxConcurrentOperationCount 屬性來控制一個特定隊列中可以有多少個操作參與併發執行。將其設置爲 1 的話,你將得到一個串行隊列,這在以隔離爲目的的時候會很有用。另外還有一個方便的功能就是根據隊列中 operation 的優先級對其進行排序,這不同於 GCD 的隊列優先級,它隻影響當前隊列中所有被調度的 operation 的執行先後。如果你需要進一步在除了 5 個標準的優先級以外對 operation 的執行順序進行控制的話,還可以在 operation 之間指定依賴關係,如下:

[intermediateOperation addDependency:operation1];

[intermediateOperation addDependency:operation2];

[finishedOperation addDependency:intermediateOperation];這些簡單的代碼可以確保 operation1operation2intermediateOperation 之前執行,當然,也會在 finishOperation 之前被執行。對於需要明確的執行順序時,操作依賴是非常強大的一個機制。它可以讓你創建一些操作組,並確保這些操作組在依賴它們的操作被執行之前執行,或者在併發隊列中以串行的方式執行操作。

從本質上來看,操作隊列的性能比 GCD 要低那麼一點,不過,大多數情況下這點負面影響可以忽略不計,操作隊列是併發編程的首選工具。

四、Run loop

並不像 GCD 或者操作隊列那樣是一種併發機制,因爲它並不能並行執行任務。不過在主 dispatch/operation 隊列中, run loop 將直接配合任務的執行,它提供了一種異步執行代碼的機制。

無論何時你使用 run loop 來執行一個方法的時候,都需要記住一點:run loop 可以運行在不同的模式中,每種模式都定義了一組事件,供 run loop 做出響應。這在對應 main run loop 中暫時性的將某個任務優先執行這種任務上是一種聰明的做法。

如果你真需要在別的線程中添加一個 run loop ,那麼不要忘記在 run loop 中至少添加一個 input source 。如果 run loop 中沒有設置好的 input source,那麼每次運行這個 run loop ,它都會立即退出。


五、併發編程可能出現的問題

1.競態條件:線程 A 和 B 都從內存中讀取出了計數器的值,假設爲 17 ,然後線程A將計數器的值加1,並將結果 18 寫回到內存中。同時,線程B也將計數器的值加 1 ,並將結果 18 寫回到內存中。實際上,此時計數器的值已經被破壞掉了,因爲計數器的值 17 被加 1 了兩次,而它的值卻是 18。這個問題被叫做競態條件,在多線程裏面訪問一個共享的資源,如果沒有一種機制來確保在線程 A 結束訪問一個共享資源之前,線程 B 就不會開始訪問該共享資源的話,資源競爭的問題就總是會發生。爲了防止出現這樣的問題,多線程需要一種互斥的機制來訪問共享資源

2.互斥鎖:互斥訪問的意思就是同一時刻,只允許一個線程訪問某個特定資源。爲了保證這一點,每個希望訪問共享資源的線程,首先需要獲得一個共享資源的互斥鎖,一旦某個線程對資源完成了操作,就釋放掉這個互斥鎖,這樣別的線程就有機會訪問該共享資源了。除了確保互斥訪問,還需要解決代碼無序執行所帶來的問題。如果不能確保 CPU 訪問內存的順序跟編程時的代碼指令一樣,那麼僅僅依靠互斥訪問是不夠的。爲了解決由 CPU 的優化策略引起的副作用,還需要引入內存屏障。通過設置內存屏障,來確保沒有無序執行的指令能跨過屏障而執行。從語言層面來說,在 Objective-C 中將屬性以 atomic 的形式來聲明,就能支持互斥鎖了。事實上在默認情況下,屬性就是 atomic 的。將一個屬性聲明爲 atomic 表示每次訪問該屬性都會進行隱式的加鎖和解鎖操作。雖然最把穩的做法就是將所有的屬性都聲明爲 atomic,但是加解鎖這也會付出一定的代價。

3.死鎖

互斥鎖解決了競態條件的問題,但很不幸同時這也引入了一些其他問題,其中一個就是死鎖。當多個線程在相互等待着對方的結束時,就會發生死鎖,這時程序可能會被卡住。

再說一次,你在線程之間共享的資源越多,你使用的鎖也就越多,同時程序被死鎖的概率也會變大。這也是爲什麼我們需要儘量減少線程間資源共享,並確保共享的資源儘量簡單的原因之一。

4.資源飢餓

如果一個持有讀取鎖的線程在等待獲取寫入鎖的時候,其他希望讀取資源的線程則因爲無法獲得這個讀取鎖而導致資源飢餓的發生。

5.優先級反轉

優先級反轉是指程序在運行時低優先級的任務阻塞了高優先級的任務,有效的反轉了任務的優先級。由於 GCD 提供了擁有不同優先級的後臺隊列,甚至包括一個 I/O 隊列,所以我們最好了解一下優先級反轉的可能性。

但另一方面,併發實際上是一個非常棒的工具。它充分利用了現代多核 CPU 的強大計算能力。在開發中,關鍵的一點就是儘量讓併發模型保持簡單,這樣可以限制所需要的鎖的數量。

我們建議採納的安全模式是這樣的:從主線程中提取出要使用到的數據,並利用一個操作隊列在後臺處理相關的數據,最後回到主隊列中來發送你在後臺隊列中得到的結果。使用這種方式,你不需要自己做任何鎖操作,這也就大大減少了犯錯誤的機率。




Grand Central Dispatch (GCD)Apple開發的一個多核編程的解決方法。

dispatch queue分成以下三種:

1)運行在主線程的Main queue,通過dispatch_get_main_queue獲取。

可以看出,dispatch_get_main_queue也是一種dispatch_queue_t

2)並行隊列global dispatch queue,通過dispatch_get_global_queue獲取,由系統創建三個不同優先級的dispatch queue。並行隊列的執行順序與其加入隊列的順序相同。

3)串行隊列serial queues一般用於按順序同步訪問,可創建任意數量的串行隊列,各個串行隊列之間是併發的。

當想要任務按照某一個特定的順序執行時,串行隊列是很有用的。串行隊列在同一個時間只執行一個任務。我們可以使用串行隊列代替鎖去保護共享的數據。和鎖不同,一個串行隊列可以保證任務在一個可預知的順序下執行。

serial queues通過dispatch_queue_create創建,可以使用函數dispatch_retaindispatch_release去增加或者減少引用計數。

GCD的用法


 //  後臺執行:

 dispatch_async(dispatch_get_global_queue(0, 0), ^{

      // something

 });

 // 主線程執行:

 dispatch_async(dispatch_get_main_queue(), ^{

      // something

 });

 // 一次性執行:

 static dispatch_once_t onceToken;

 dispatch_once(&onceToken, ^{

     // code to be executed once

 });

 // 延遲2秒執行:

 double delayInSeconds = 2.0;

 dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);

 dispatch_after(popTime, dispatch_get_main_queue(), ^(void){

     // code to be executed on the main queue after delay

 });

 // 自定義dispatch_queue_t

 dispatch_queue_t urls_queue = dispatch_queue_create("blog.devtang.com", NULL);

 dispatch_async(urls_queue, ^{  

   // your code 

 });

 dispatch_release(urls_queue);

 // 合併彙總結果

 dispatch_group_t group = dispatch_group_create();

 dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{

      // 並行執行的線程一

 });

 dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{

      // 並行執行的線程二

 });

 dispatch_group_notify(group, dispatch_get_global_queue(0,0), ^{

      // 彙總結果

 });


GCD的另一個用處是可以讓程序在後臺較長久的運行。

在沒有使用GCD時,當app被按home鍵退出後,app僅有最多5秒鐘的時候做一些保存或清理資源的工作。但是在使用GCD後,app最多有10分鐘的時間在後臺長久運行。這個時間可以用來做清理本地緩存,發送統計數據等工作。

讓程序在後臺長久運行的示例代碼如下:


// AppDelegate.h文件

@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundUpdateTask;


// AppDelegate.m文件

- (void)applicationDidEnterBackground:(UIApplication *)application

{

    [self beingBackgroundUpdateTask];

    // 在這裏加上你需要長久運行的代碼

    [self endBackgroundUpdateTask];

}


- (void)beingBackgroundUpdateTask

{

    self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{

        [self endBackgroundUpdateTask];

    }];

}


- (void)endBackgroundUpdateTask

{

    [[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask];

    self.backgroundUpdateTask = UIBackgroundTaskInvalid;

}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章