OS開發基礎——多執行緒的簡單應用

joker_jm發表於2019-02-15

小記

目前最廣泛被認知的幾種多執行緒有:

  • pthread
  • NSThread
  • GCD
  • NSOperation

其中常用的是後面兩種:GCD 和 NSOperation,這邊文章就是對後兩種常見用法的簡單介紹,以及前兩種 pthread 和 NSThread 的簡單說明。

pthread

一套通用的多執行緒API,適用於Unix\Linux\Windows等系統,跨平臺,可移植性強,由純C語言編寫的API,且執行緒的生命週期需要程式設計師自己管理,使用難度較大,所以在實際開發中通常不使用,在這裡也不詳細說明。

注意:在使用pthread的時候一定要手動把當前執行緒結束掉。如果有想從底層進行定製多執行緒的操作,可以使用ptherad。

NSThread

基於OC語言由蘋果進行封裝的API,使得其簡單易用,完全物件導向操作。執行緒的宣告週期由程式設計師管理,在實際開發中偶爾使用。

簡單使用:

建立執行緒:
  • 使用NSThread的init方法顯式建立
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod) object:nil];
//執行緒名
[thread setName:@"thread"];
//優先順序 優先順序從0到1
[thread setThreadPriority:0.9];
//啟動
[thread start];
複製程式碼
  • 使用NSThread類方法顯式建立並啟動執行緒
[NSThread detachNewThreadSelector:@selector(threadMethod:) toTarget:self withObject:nil];
複製程式碼
  • 隱式建立並啟動執行緒
[self performSelectorInBackground:@selector(threadMethod:) withObject:nil];
複製程式碼

注意:新增執行緒的名字、更改優先順序等操作,要使用第一種方式來建立執行緒。因為只有使用NSThread的init方法建立的執行緒才會返回具體的執行緒例項,此方法需要使用start方法來手動啟動執行緒。

執行緒狀態:
  • 啟動
[thread start];
複製程式碼
  • 阻塞
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
[NSThread sleepForTimeInterval:1];
複製程式碼
  • 結束(注意:當使用cancel方法時,只是改變了執行緒的狀態標識,並不是結束執行緒,要配合isCancelled方法進行判斷,退出執行緒使用)
[[NSThread currentThread] cancel];
if([[NSThread currentThread] isCancelled]) {
    [NSThread exit];//執行exit,後邊的語句不再執行,可以通過 start 再次啟動執行緒
}
if([[NSThread currentThread] isCancelled]) {
    return;//後邊的語句不再執行,不可以通過 start 再次啟動執行緒
}
複製程式碼
執行緒通訊:

執行緒間通訊,最常用的就是開啟子執行緒進行耗時操作,操作完畢後回到主執行緒,進行資料賦值以及重新整理主執行緒UI。

[self performSelectorOnMainThread:@selector(backToMainThread:) withObject:image waitUntilDone:YES];
複製程式碼
[self performSelector:@selector(backToMainThread:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
複製程式碼
執行緒安全:

多執行緒操作會存在一定的安全隱患。原因是多執行緒會存在不同執行緒的資源共享,也就是說我們可能在同一時刻兩個執行緒同時操作了某一個變數的值(資源競爭),但是執行緒的對變數的操作不同,導致變數的值出現誤差。

例如:如果有一個變數x = 100,有兩個執行緒A和B,A執行緒取x的值(x=100),B執行緒取x的值(x=100),B執行緒給x+1 (x=101),A執行緒給x+1 (x = 101),B 執行緒取x的值 (x = 101)或者( x = 102 )。變數出行了誤差。

解決方案新增執行緒鎖,有多種執行緒鎖,在這裡不多介紹。

iOS開發基礎——執行緒安全(程式鎖)

GCD

基於C語言編寫由蘋果公司提供的的一套多執行緒開發解決方案,使用時會以函式形式出現,且大部分函式以dispatch開頭。它會自動利用多核進行併發處理和運算,它能提供系統級別的處理,而不再侷限於某個程式、執行緒,執行緒的生命週期由系統自動管理(建立,排程、執行,銷燬),只需要告訴GCD執行什麼任務,不需要編寫管理執行緒的程式碼。

基本使用

建立列隊和任務:

建立列隊(queue)
  • 序列列隊(一次執行一個任務)
dispatch_queue_t queue = dispatch_queue_create("10900900",DISPATCH_QUEUE_SERIAL);
複製程式碼
  • 併發列隊(一次可執行多個任務)
dispatch_queue_t queue = dispatch_queue_create("10900901",DISPATCH_QUEUE_CONCURRENT);
複製程式碼
  • 全域性列隊(本質是一個併發佇列,由系統提供,所有應用程式共享的,方便程式設計,可以不用建立就直接使用)
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
複製程式碼
  • 主列隊(專門排程主執行緒的任務,不開闢新的執行緒。在主佇列下的任務不管是非同步還是同步都不會開闢新執行緒,任務只會在主執行緒順序執行)
dispatch_queue_t queue = dispatch_get_main_queue();
複製程式碼
建立任務(sync 和 async)
  • 同步任務(同步序列和同步併發,任務執行的方式是一樣的,沒有開闢新的執行緒,所有的任務都是在一條執行緒裡面執行。)
dispatch_sync(queue, ^{
});
複製程式碼
  • 非同步任務(非同步序列和非同步併發,任務執行的方式是有區別的,非同步序列會開闢一條新的執行緒,佇列中所有任務按照新增的順序一個一個執行,非同步併發會開闢多條執行緒,至於具體開闢多少條執行緒,是由系統決定的。)
dispatch_async(queue, ^{
});
複製程式碼
列隊和任務的組合各種情況分析:
  • 同步任務,併發列隊
  • 所有任務都是在當前執行緒中執行,沒有開啟新的執行緒(同步方法不具備開啟新執行緒的能力)
  • 同步任務需要等候列隊中的任務執行結束,才會執行下一個
  • 併發列隊可以開啟多執行緒,並且可以同時執行多個任務,但是同步任務無法建立新執行緒,所以只有當前一個執行緒,而且同步任務需要等待列隊中前一任務執行結束才能繼續執行下面的操作,因此任務只能一個一個順序執行
  • 同步任務,序列列隊
  • 和同步任務,併發列隊相似
  • 所有任務在當前執行緒中執行,沒有開啟新的執行緒
  • 任務是按照順序執行的,同步任務,執行緒需要等待列隊中的任務執行完畢,才可以開啟新的任務
  • 同步任務,主列隊
  • 在主執行緒中呼叫會出現死鎖,互相等待
  • 死鎖原因:當我們在主執行緒中新增這個列隊的時候,新增列隊的這個操作本身就是一個任務,我們把它當作任務A,這個任務也被新增到了主執行緒的列隊中。而同步任務,會等待當前列隊中前面的任務執行完畢後接著執行,我們把新增到主執行緒中的列隊中的任務稱為任務B,這就產生了一個矛盾,任務B要執行需要等任務A執行完畢後才會執行,而任務A執行完畢需要任務B執行結束(因為任務B在任務A中),這就產生了任務互相等待的情況
  • 非同步任務,併發列隊
  • 有幾個非同步任務就開啟了幾個新的執行緒,任務也是同時執行的(非同步方法具備開啟新執行緒的能力,可以同時執行多個任務)
  • 非同步執行,當前執行緒不等待,直接開啟新的執行緒來執行,在新執行緒中執行任務(非同步任務,新增非同步任務的執行緒不做等待,可繼續執行別的任務)
  • 非同步任務,序列列隊
  • 開啟了一條新的執行緒來執行非同步任務(非同步任務可以開啟新執行緒,序列列隊只能開啟一個執行緒)
  • 執行緒不會等待任務執行完畢,任務的執行是按照順序來的,每次只有一個任務被執行,任務一個接一個的執行下去
  • 非同步任務,主列隊
  • 沒有開啟新執行緒,所有任務都是在主執行緒中執行的(雖然非同步任務有開啟新執行緒的能力,但因為是在主列隊,所以無法開啟新執行緒,所有任務都在主執行緒中執行)
  • 由於只有一個執行緒可以使用,所以所有任務都是按順序一個個執行的,一個完畢,執行下一個

執行緒間的通訊

執行緒間的通訊比較常用的就是在其他執行緒獲取資料,然後返回主執行緒重新整理UI介面

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
    // 非同步追加任務
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              
       NSLog(@"1---%@",[NSThread currentThread]);     
    }
    // 回到主執行緒
    dispatch_async(mainQueue, ^{
        [NSThread sleepForTimeInterval:2];               
        NSLog(@"2---%@",[NSThread currentThread]);      
    });
});
複製程式碼

進階使用

GCD柵欄

有時我們需要非同步執行兩組操作分別為A組和B組,當A組完成後,再執行B組操作,因此我們需要把把兩組操作分割開來。這時可用dispatch_barrier_async方法來實現,在新增兩組任務之間新增一個分欄,函式會先把分欄前新增的任務執行完畢之後,在把分欄後的任務新增到佇列中

dispatch_async(queue, blk1);
dispatch_async(queue, blk2);
dispatch_barrier_async(queue, barrierBlk);
dispatch_async(queue, blk3);
dispatch_async(queue, blk4);
複製程式碼

柵欄函式也可以執行佇列上的操作(引數列表中有queue和block),也有對應的 dispatch_barrier_sync 函式。

柵欄函式中傳入的引數佇列必須是由 dispatch_queue_create 方法建立的佇列,否則,與dispatch_async無異,起不到“柵欄”的作用了,對於dispatch_barrier_sync也是同理。

柵欄函式之前和之後的操作執行順序都不固定,但是前面三個必然先執行,然後再執行柵欄函式中的操作,最後執行後面的三個

dispatch_barrier_syncdispatch_barrier_async 的區別:

  • 當柵欄前後新增的都是同步任務,兩者沒有區別,按照順序依次執行
  • 當柵欄前後新增的是非同步任務,sync 會先執行柵欄前的任務,然後不等待柵欄後部任務。async柵欄前後的任務都不等待

由此可見sync和async對於柵欄函式的區別作:dispatch_barrier_sync將自己的任務插入到佇列的時候,需要等待自己的任務結束之後才會繼續插入被寫在它後面的任務,然後執行它們。dispatch_barrier_async將自己的任務插入到佇列之後,不會等待自己的任務結束,它會繼續把後面的任務插入到佇列,然後等待自己的任務結束後才執行後面任務

GCD延遲

當遇到要求在指定時間後執行程式碼(例如5秒後執行程式碼),可用dispatch_after來實現,需要注意的是這個並不嚴謹,這個是指在指定時間後,再把程式碼加入列隊中去,並不是嚴格的在多少時間後開始執行

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(second * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // second秒後非同步追加任務程式碼到主佇列,並開始執行
    NSLog(@"after---%@",[NSThread currentThread]);  // 列印當前執行緒
});
複製程式碼

GCD一次性程式碼

在建立單例,或者有程式碼要求在整個程式的執行過程中之執行一次的話可以使用GCD中的dispatch_once 函式,這個函式保證即使在多執行緒的環境下也可以保證只呼叫一次,保證執行緒安全

- (void)once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    // 只執行1次的程式碼(這裡面預設是執行緒安全的)
    });
}
複製程式碼

GCD快速迭代

快速迭代函式dispatch_apply按照指定的次數把指定的任務加入到指定的列隊中去,並等待全部的任務執行完畢後,結束

- (void)applyTime:(NSInteger)time {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSLog(@"apply---begin");
    dispatch_apply(time, queue, ^(size_t index) {
        NSLog(@"%zd---%@",index, [NSThread currentThread]);
    });
    NSLog(@"apply---end");
}
複製程式碼

注意:由於是在併發列隊中非同步執行,所以裡面的執行完成順序不固定

GCD訊號量

GCD中的訊號量,是一種持有計數的訊號,計數為0時,不可通過,要等待。計數為1或大於1時,可通過,不需等待。

三個函式來完成訊號量的操作
  • dispatch_semaphore_create:建立一個Semaphore並初始化訊號的總量
  • dispatch_semaphore_signal:傳送一個訊號,讓訊號總量加 1
  • dispatch_semaphore_wait:可以使總訊號量減1,當訊號總量為0時就會一直等待(阻塞所線上程),否則就可以正常執行。
實際開發中的作用:
  • 保持執行緒同步,使非同步執行的任務轉化為同步執行
  • 保護執行緒安全,為執行緒加鎖

例如:當非同步執行耗時操作時,需要使用該非同步操作的結果進行一些額外的操作,例如:同時進行兩個非同步操作A和B,執行B的時候,需要對A的執行結果來進行操作,這個時候就可以對B加一個訊號量,讓B等待,當A執行完畢後,對B操作傳送訊號,繼續執行B操作

- (void)semaphoreSync {
    NSLog(@"semaphore---begin");
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //建立初始訊號量 為 0 ,阻塞所有執行緒
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任務A
        [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 列印當前執行緒
        number = 100;
        // 執行完執行緒,訊號量加 1,訊號總量從 0 變為 1
        dispatch_semaphore_signal(semaphore);
    });
    //原任務B
    ////若計數為0則一直等待,直到接到總訊號量變為 >0 ,繼續執行後續程式碼
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %d",number);
}
複製程式碼

例如:執行緒安全方面:若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒安全的;若有多個執行緒同時執行寫操作(更改變數),一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。(可理解為執行緒 A 和 執行緒 B 一塊配合,A 執行到一定程度時要依靠執行緒 B 的某個結果,於是停下來,示意 B 執行;B 依言執行,再將結果給 A;A 再繼續操作。)

GCD佇列組

有時候會遇到需要非同步執行兩個耗時任務,然後當兩個任務都執行完畢後,在回到主執行緒執行任務,這時候我們可以用GCD的佇列組的功能來實現這個要求

GCD佇列組常用函式:
  • 呼叫佇列組的 dispatch_group_async 先把任務放到列隊中,然後把列隊放入到列隊組中。也可用dispatch_group_enterdispatch_group_leave 兩個組合來實現 dispatch_group_async
  • dispatch_group_enter:加入,一個任務追加到group,執行一次,相當於group中的未執行任務加1
  • dispatch_group_leave:離開,一個任務離開了group,執行一次,相當於group中的未執行任務減1

當group中的未執行完畢的任務數為0的時候才會執行dispatch_group_notify中的任務,以及不會使dispatch_group_wait堵塞當前執行緒(類似於訊號量)

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_enter(group);
dispatch_sync(queue, ^{
    // 追加任務A
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 列印當前執行緒
    }
    dispatch_group_leave(group);
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
複製程式碼
  • 呼叫佇列組的 dispatch_group_notify 回到指定執行緒執行任務(監聽group中的所有任務的完成狀態,當所有任務都完成後,把notify中的任務新增到group中,並執行任務)
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 追加任務A
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 列印當前執行緒
    }
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 追加任務B
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 列印當前執行緒
    }
});
    
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 追加任務Main
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
        NSLog(@"main---%@",[NSThread currentThread]);      // 列印當前執行緒
    }
});
複製程式碼
  • 呼叫佇列組的 dispatch_group_wait 回到當前執行緒繼續向下執行(暫停當前執行緒中的操作,阻塞當前執行緒,執行wait中的group操作,執行完後,繼續執行當前執行緒中的操作)
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 追加任務A
    for (int i = 0; i < 2; ++i) {
        [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 列印當前執行緒
    }
});
//執行A任務,執行完成後繼續執行該執行緒後續任務
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"current---%@",[NSThread currentThread]);
複製程式碼

NSOperation

基於OC語言的API,底層是GCD,增加了一些更加簡單易用的功能,使用更加物件導向。執行緒生命週期由系統自動管理,在實際開發中經常使用。

優勢:

  1. 可以新增代完成的程式碼塊,在操作完成後執行
  2. 新增操作之間的依賴關係,控制執行順序
  3. 設定操作執行的優先順序
  4. 可以很方便的取消一個操作的執行
  5. 使用KVO觀察物件操作執行狀態的更改:isExecuteing(執行) isFinished(結束) isCancelled(取消)

兩個重要概念操作和操作佇列:

NSOperation(操作):

  • 執行緒中執行的操作程式碼
  • 與GCD不同,GCD是放在block塊中。在NSOperation中,可以使用它的子類NSInvocationOperationNSBlockOperation來實現,也可以自定義子來封裝操作

NSOperationQueue(佇列):

  • 這個列隊不同於GCD中的列隊中先進先出的原則,這裡的列隊對於新增到列隊中的操作首先進入準備就緒的狀態(這個根據操作之間的依賴關係來決定),然後進可以開始執行的操作的執行順序要先按照操作之間的相對的優先順序來決定,然後再根據進入的順序決定執行順序
  • 操作的佇列通過設定 最大併發運算元 (maxConcurrentOperationCount)來控制併發和序列
  • NSOperationQueue提供了兩種不同的列隊,主列隊和自定義列隊。主列隊執行在主執行緒之上,自定義列隊在後臺執行。

基本使用:

建立操作:

  • 使用子類 NSInvocationOperation
-(void)useNSInvocationOperation{
    NSInvocationOperation * op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationTest) object:nil];
    [op start];
}
複製程式碼

是在沒有使用NSOperationQueue的情況下,在主執行緒中單獨使用子類執行一個操作,操作是在當前執行緒執行的,並沒有開啟新執行緒,想要在其他執行緒來執行這個操作的話可以使用:

[NSThread detachNewThreadSelector:@selector(useNSInvocationOperation) toTarget:self withObject:nil];
複製程式碼
  • 使用子類 NSBlockOperation
-(void)useNSBlockOperation{
    NSBlockOperation * op = [NSBlockOperation blockOperationWithBlock:^  {
        for (NSInteger i = 0 ; i < 2 ; i ++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1---%@", [NSThread currentThread]);
        }
    }];
    [op start];
}
複製程式碼

和上一個一樣也是在當前執行緒呼叫,想要在其他執行緒使用的話可以:

[NSThread detachNewThreadSelector:@selector(useNSBlockOperation) toTarget:self withObject:nil];
複製程式碼

注意:NSBlockOperation有方法可以新增額外的操作addExecutionBlock:

[op addExecutionBlock:^{
    for (NSInteger i = 0 ; i < 2 ; i ++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2---%@", [NSThread currentThread]);
    }
}];
複製程式碼

可以在不同的執行緒中併發執行,只有所有新增的操作全部完成,才視為這個block操作完成。一般情況下,一個operation物件如果封裝了多個操作,是否開啟新執行緒來執行這些操作,取決於操作的個數,由系統來決定是否開啟新執行緒。

  • 使用自定義子類繼承自NSOperation

自定義的子類可以通過重寫 main 或者 start 方法來自定義操作物件 重寫 main 方法比較簡單,不用管理狀態屬性 isExecuting 等。

-(void)useCustomOperation{
    JMOperation * op = [[JMOperation alloc] init];
    [op start];
}
複製程式碼

操作的具體實現寫在重寫的類中,方便管理和統一修改。

建立列隊:

  • 主列隊
    • 凡是新增的主列隊中的操作都在主執行緒中執行
    NSOperationQueue * queue = [NSOperationQueue mainQueue];
    複製程式碼
  • 自定義列隊
    • 新增到自定義列隊中的,自動放在子執行緒執行,後臺執行
    • 同時包含了多種功能:序列,併發
    NSOperationQueue * queue = [[NSOperationQueue alloc] init];
    複製程式碼

加入佇列:

  • 將建立好的操作加入到列隊中
    -(void)createCustomQueue{
        NSOperationQueue * queue = [[NSOperationQueue alloc] init];
        NSInvocationOperation * op_1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationTest) object:nil];
        NSBlockOperation * op_2 = [NSBlockOperation blockOperationWithBlock:^  {
            for (NSInteger i = 0 ; i < 2 ; i ++) {
                [NSThread sleepForTimeInterval:2];
                NSLog(@"2---%@", [NSThread currentThread]);
            }
        }];
        [op_2 addExecutionBlock:^{
            for (NSInteger i = 0 ; i < 2 ; i ++) {
                [NSThread sleepForTimeInterval:2];
                NSLog(@"3---%@", [NSThread currentThread]);
            }
        }];
        [queue addOperation:op_1]; // 操作加入佇列
        [queue addOperation:op_2];
    }
    複製程式碼
  • 直接在列隊中建立操作
    -(void)createMainQueue{
        NSOperationQueue * queue = [NSOperationQueue mainQueue];
        // 佇列加入block方法
        [queue addOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
             NSLog(@"1---%@", [NSThread currentThread]); // 列印當前執行緒
            }
        }];
        [queue addOperationWithBlock:^{
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
                NSLog(@"2---%@", [NSThread currentThread]); // 列印當前執行緒
            }
        }];
    }
    複製程式碼

進階使用:

NSOperationQueue控制序列執行,併發執行:

關鍵詞(maxConcurrentOperationCount),最大併發運算元,用來控制一個列隊中可以有多少個操作同時併發執行

注意:這個數值控制的並不是併發執行緒的數量,而是一個佇列中同時能併發執行的最大運算元,一個操作並非只能在一個執行緒中執行。

開啟執行緒的數量由系統決定,無法人為管理。 queue.maxConcurrentOperationCount = 4;

  • 預設為-1,表示不限制,可以進行併發執行
  • 值為1時,表示只可序列執行
  • 大於1時,表示併發執行,不可超過系統限制

NSOperation操作依賴:

NSOperation的獨有功能,操作依賴,通過操作依賴,我們可以根據設定的依賴關係,很方便的控制操作的執行順序,NSOperation提供3個介面供我們管理和檢視依賴

  • [op_1 addDependency:op_2];
  • [op_1 removeDependency:op_2];
  • @property (readonly, copy) NSArray<NSOperation *> *dependencies;

上面的三個操作分別是新增依賴,移除依賴,獲取當前操作所依賴的所有操作的陣列

NSOperation優先順序:

NSOperation提供了 queuePriority (優先順序)屬性。queuePriority 屬性適用於同一操作列隊中的操作,不適用於不同操作列隊中的操作。預設情況下,所有操作物件優先順序都是 NSOperationQueuePriorityNormal 。但是我們可以通過setQueuePriority: 方法來改變當前操作在同一個列隊中的優先順序:

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
 	NSOperationQueuePriorityVeryLow = -8L,
 	NSOperationQueuePriorityLow = -4L,
 	NSOperationQueuePriorityNormal = 0,
 	NSOperationQueuePriorityHigh = 4,
 	NSOperationQueuePriorityVeryHigh = 8
};
複製程式碼

注意:

  • 依賴關係 優先於 優先順序 屬性,先判斷依賴關係,再執行優先順序的順序
  • 當屬於同一個依賴關係時,優先順序高的先執行。
  • 優先順序只是確保執行順序,無法保證執行完成順序

NSOperation 和 NSOperationQueue執行緒間的通訊:

NSOperationQueue * queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
    for (NSInteger i = 0 ; i < 2 ; i ++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2---%@", [NSThread currentThread]);
    }
}];
    
    //返回主執行緒
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    // 進行一些 UI 重新整理等操作
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
        NSLog(@"2---%@", [NSThread currentThread]); // 列印當前執行緒
    }
}];
複製程式碼

通過[NSOperationQueue mainQueue]函式返回主執行緒的操作

NSOperation 和 NSOperationQueue 的執行緒安全和執行緒鎖:

多執行緒有多種枷鎖方式來保護執行緒安全,常用的是NSLock,通過NSLock來給程式加鎖,解鎖。加鎖後其他程式無法再訪問這個方法和屬性

  • NSLock * lock = [[NSLock alloc] init];
  • [lock lock];
  • [lock unlock];

初始化lock方法,加鎖,解鎖方法

某個A執行緒呼叫lock方法,這樣,nslock將被上鎖,可以執行被鎖住的關鍵方法,完成後A執行緒呼叫unlock方法解鎖。如果在A執行緒呼叫unlock方法之前,有B執行緒需要呼叫被鎖住的關鍵方法,那麼將無法訪問,一直等待,直到A執行緒呼叫了unlock方法。

其他還有多種鎖的方式,如:自旋鎖,互斥鎖,遞迴鎖,條件鎖,讀寫鎖等,具體程式鎖在下篇文章再來介紹。iOS開發基礎——執行緒安全(程式鎖)

相關文章