iOS 深入剖析多執行緒

lazy_boy發表於2017-12-19

前言

成長總是在不斷的受挫中,反思,改進,最後成長

同步 vs 非同步

同步和非同步的維度是執行緒,區別是在當前執行的任務中,是否會阻塞當前的執行緒,如果是同步的會阻塞當前執行緒。非同步的話,不會阻塞當前執行緒,他會開闢一個新的執行緒來執行該任務。 這麼說,執行緒是用來執行任務的,佇列是通過管理執行緒,來決定任務的執行方式。

NSOperation vs GCD

  • GCD是蘋果基於c語言構成的API,而NSOperation是GCD的封裝
  • 在NSOperationQueue,我們可以隨時取消已經設定要準備執行的任務(已經開始的任務就無法阻止),而GCD停止準備執行的任務是比較困難的,沒有NSOperationQueue方便
  • NSOperation 能夠方便的設定依賴關係,我們可以讓一個NSOperation依賴另外一個NSOperation,這樣的話,儘管兩個NSOperation在同一個佇列中,但前者直到後者執行完畢後再執行

NSOperation

NSOperation是OC層面的對外提供的物件導向的執行緒管理類,是基於GCD的封裝。你定義想要執行的任務,他來負責排程和執行這些任務,它管理了執行緒,並且使執行緒更加高效。

  • 管理執行緒,負責執行緒的建立和銷燬,通過配置佇列,讓任務按照你想要的方式執行。
NSOperationQueue + NSOperation

NSOperation 任務,NSOperationQueue 佇列,建立任務新增到佇列中。佇列有來決定任務的執行方式:

  • 併發執行,序列執行
  • 可以同時執行的併發數
  • 暫停任務,開始任務
NSBlockOperation *operation = [NSBlockOperation 
   blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
    }];
    [operation start];
複製程式碼

上面程式碼的意思是把operation任務新增到執行緒中執行。 NSBlockOperation 是一個繼承NSOperation的類

addExecutionBlock

當然一個任務也不一定是一個執行緒在執行,我們可以通過addExecutionBlock給該任務新增多個執行緒來執行,當然蘋果預設了addExecutionBlock新增到任務中的執行緒是非同步的我們可以通過

if ([operation isConcurrent]) {
        NSLog(@"併發");
    } else {
        NSLog(@"非併發");
    }
複製程式碼

來檢視是非併發還是不是併發的

NSBlockOperation *operation = [NSBlockOperation 
blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
    }];

    // 新增多個block
    for (NSInteger i = 0; i < 5; i++) {
        [operation addExecutionBlock:^{
            NSLog(@"第%ld次: %@",(long)i, [NSThread currentThread]);
        }];
    }
    [operation start];
複製程式碼
iOSThread[8646:497795] 第0次: <NSThread: 0x60000026dc40>{number = 4, name = (null)}
iOSThread[8646:497794] <NSThread: 0x60400026d840>{number = 3, name = (null)}
iOSThread[8646:497793] 第2次: <NSThread: 0x60400026db40>{number = 5, name = (null)}
iOSThread[8646:497585] 第1次: <NSThread: 0x604000068300>{number = 1, name = main}
iOSThread[8646:497793] 第4次: <NSThread: 0x60400026db40>{number = 5, name = (null)}
iOSThread[8646:497794] 第3次: <NSThread: 0x60400026d840>{number = 3, name = (null)}
複製程式碼

可以看到addExecutionBlock,可以為該任務新增併發執行緒。從上面可以看出最大併發數除了主執行緒為還有4個,並且會把block優先分配給主執行緒,當主執行緒不在空閒時,才會選擇分配到其他執行緒來執行。

設定程式碼優先順序:

NSBlockOperation *downloadPic = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1 ----- %@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:2.0]; // 模擬下載操作
    }];
    [downloadPic setQueuePriority:NSOperationQueuePriorityVeryLow]; // 設定在佇列中的優先順序,較低

    NSBlockOperation *downloadMusic = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"2 ----- %@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:2.0f]; // 模擬下載操作
    }];
    [downloadMusic setQueuePriority:NSOperationQueuePriorityVeryHigh];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:downloadPic];
    [queue addOperation:downloadMusic];
複製程式碼

列印程式碼:

iOSThread[9549:632233] 1 ----- <NSThread: 0x604000461040>{number = 3, name = (null)}
iOSThread[9549:632235] 2 ----- <NSThread: 0x60000026c240>{number = 4, name = (null)}
複製程式碼

可以看出,佇列中的任務不需要呼叫start來執行。而且,新增到佇列中的任務在設定優先順序之後,並不一定會優先執行。對於新增到 queue的Operations,執行順序首先由已入佇列的operations是否準備好,然後在根據所有operations的相對優先順序確定。是否準備好由物件間的依賴關係確定,優先順序等級則是operations物件本身的一個屬性。預設所有operation都擁有"普通優先順序",不過可以通過設定setQueuePriority方法,來降低或提升operation物件的優先順序。優先順序只能應用於相同queue中的operations。佇列中執行任務,需要先滿足依賴關係,在根據優先順序來執行。

在執行的任務當中新增依賴關係
 // 新增依賴
 //1.任務一:下載圖片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"下載圖片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//2.任務二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"打水印   - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//3.任務三:上傳圖片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"上傳圖片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//4.設定依賴
[operation2 addDependency:operation1];      //任務二依賴任務一
[operation3 addDependency:operation2];      //任務三依賴任務二

//5.建立佇列並加入任務
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
複製程式碼

佇列可以有多個,任務是在加入到佇列中執行的,佇列中任務的執行方式是由佇列來決定的,是並行佇列,還是序列佇列。佇列與佇列中的執行方式是並行的,每個佇列管理這自己建立的執行緒。

NSInvocationOperation

這個類是繼承自抽象類NSOperation的

NSInvocationOperation *operation = [[NSInvocationOperation 
alloc] initWithTarget:self selector:@selector(demo) object:nil];
    [operation start];
複製程式碼

從列印結果上來看,這個類的執行是同步的,會在當前執行緒中執行。

GCD

GCD 提供C語言級別的執行緒管理排程,建立執行緒,銷燬執行緒,都是由GCD來幫我們實現 GCD 提供兩種佇列,併發和序列佇列

// 建立佇列的時候可以設定該佇列中任務的執行方式是序列的
DISPATCH_QUEUE_SERIAL
// 並行佇列
DISPATCH_QUEUE_CONCURRENT
複製程式碼
dispatch_queue_t
複製程式碼

在序列佇列中,所有的任務都是以FIFO的形式執行,且當前佇列中的任務執行完畢,才開始執行下一個任務。但是,相互獨立的佇列之間的任務的執行是可以並行的,不受另外一個佇列的影響。也就是佇列與佇列之間任務的執行方式是獨立的。 在併發佇列中,任務的執行方式也是以FIFO(先進先出)的形式來執行的,只不過他不需要等待上一個任務執行結束,而且該佇列會幫助我們建立新的執行緒來併發執行該任務。

// 主佇列,序列佇列
dispatch_get_main_queue()
// 全域性併發佇列,只能獲取不能自己建立,可以為這個佇列設定識別符號,和對用的優先順序
dispatch_get_global_queue(long identifier, unsigned long flags)
複製程式碼
dispatch_get_main_queue()

如下面一段程式碼:

dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"執行任務");
    });
複製程式碼

把該任務加入到主佇列中,同步執行

dispatch_get_global_queue(long identifier, unsigned long flags)

上面我們說了,系統提供的全域性併發佇列我們只可以獲取但是不能夠建立。我們自己建立的佇列都是採用與全域性佇列一樣的優先順序。如果,我們想要設定更高優先順序的佇列,可以通過獲取全域性佇列的方式,設定他的優先順序從而獲得更高優先順序的佇列:

 dispatch_queue_t queue1 = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0);
// 高優先順序
QOS_CLASS_USER_INITIATED
// 預設優先順序
QOS_CLASS_DEFAULT
// 低優先順序
QOS_CLASS_UTILITY
// 使用者不會察覺的任務,使用他來處理預載入,或者不需要使用者互動和對時間不敏感的任務。
QOS_CLASS_BACKGROUND
複製程式碼
dispatch_set_target_queue(dispatch_object_t _Nonnull object, dispatch_queue_t _Nullable queue)

第一個引數為要設定優先順序的queue,第二個引數是參照物,即將第一個queue的優先順序和第二個queue的優先順序設定一樣

設定佇列優先順序

我們可以通過這個函式來設定我們自己建立佇列的優先順序,如下:

dispatch_queue_t testQueue = dispatch_queue_create("test1.com", DISPATCH_QUEUE_SERIAL);

    dispatch_queue_t serialQueue = dispatch_queue_create("test2.com", DISPATCH_QUEUE_SERIAL);

    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_set_target_queue(serialQueue, globalQueue);

    dispatch_async(testQueue, ^{
        NSLog(@"執行1");
    });

    dispatch_async(serialQueue, ^{
        NSLog(@"執行2");
    });
複製程式碼

執行結果:

2017-12-17 18:22:16.766282+0800 iOSThread[14568:1310876] 執行2
2017-12-17 18:22:16.766282+0800 iOSThread[14568:1310875] 執行1
複製程式碼

從執行結果上來看,確實設定後的優先順序較高

把當前佇列任務指派到其他佇列中處理
dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_CONCURRENT);

    dispatch_set_target_queue(queue2, queue1);

    dispatch_async(queue2, ^{
        for (NSInteger i = 0; i < 20; i++) {
            NSLog(@"queue2:%@, %ld", [NSThread currentThread], i);
        }
    });

    dispatch_async(queue1, ^{
        for (NSInteger i = 0; i < 20; i++) {
            NSLog(@"queue1:%@, %ld", [NSThread currentThread], i);
        }
    });
複製程式碼

就是把當前queue2中的任務指派到queue1中去執行,從上面程式碼可以看出queue1是序列佇列,那麼queue2中的任務就會在queue1中以序列的方式執行。

dispatch_after
dispatch_time
dispatch_time(dispatch_time_t when, int64_t delta)
第一個引數可以設定當前時間: DISPATCH_TIME_NOW
第二個引數delta表示納秒,可以直接使用NSEC_PER_SEC
複製程式碼

那麼我們可以直接設定延時任務

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"延時執行的任務");
    });
複製程式碼

這段程式碼的意思是,這個函式等待到指定的時間。然後把任務非同步新增到指定佇列執行 既延時2s,然後把任務新增到主佇列執行

dispatch_queue_set_specific & dispatch_get_specific

dispatch_queue_set_specific 在指定佇列中設定一個標識 dispatch_get_specific 在當前佇列中取出標識

dispatch_group

把一組任務提交到佇列當中,這些佇列可以不相關,然後監聽這組任務的完成

dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t queue1 = dispatch_queue_create("test.com", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{ 
        for (int i = 0; i < 30; i++) {
            NSLog(@"group1 -%d - %@",i, [NSThread currentThread]);
        }
    });

    dispatch_group_async(group, queue1, ^{
        for (int i = 0; i < 30; i++) {
            NSLog(@"group2 - %d - %@", i, [NSThread currentThread]);
        }
    });

    //完成之後回撥
    dispatch_group_notify(group, queue, ^{
        NSLog(@"完成任務 - %@",[NSThread currentThread]);
    });
複製程式碼
dispatch_barrier_async

當在併發佇列中遇到一個barrier,這個方法會阻塞queue(不是阻塞當前執行緒)。一直等到排在這個queue前面的任務執行完後才開始執行自己,自己執行完畢後,再會取消阻塞。使這個queue中排在它後面的任務繼續執行。如果你傳入的是其他queue,那麼就和dispatch_async一樣了

dispatch_queue_t queue = dispatch_queue_create("sdk.com", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2.f];
        NSLog(@"task 1");
    });

    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2.f];
    });

    // 等待前面的都完成,在執行 barrier 後面的
    dispatch_barrier_async(queue, ^{
        NSLog(@"task 2");
        [NSThread sleepForTimeInterval:2.f];
    });

    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:3.f];
        NSLog(@"task 3");
    });

    dispatch_async(queue, ^{
        NSLog(@"task 4");
    });
複製程式碼
2017-12-18 21:51:09.001390+0800 iOSThread[37907:2827277] task 1
2017-12-18 21:51:09.001733+0800 iOSThread[37907:2827277] task 2
2017-12-18 21:51:11.005180+0800 iOSThread[37907:2827274] task 4
2017-12-18 21:51:14.008395+0800 iOSThread[37907:2827277] task 3
複製程式碼
dispatch_barrier_sync

這個方法的使用和上一個一樣,傳入自定義的併發佇列DISPATCH_QUEUE_CONCURRENT,它和上一個方法一樣的阻塞queue,不同的是,這個方法還會阻塞當前執行緒。如果,你傳入的是其他的queue,那麼就和 "dispatch_sync" 是一樣的了。

造成死鎖的幾個例子
NSLog(@"之前 - %@",[NSThread currentThread]);
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"sync - %@",[NSThread currentThread]);
    });
    NSLog(@"之後 - %@",[NSThread currentThread]);
複製程式碼

這段程式碼的執行是,先列印第一句,然後阻塞當前執行緒,把block放到 main_queue中。可以main_queue中的任務會被取出來放到主執行緒中執行,但是主執行緒已經被阻塞。這樣就造成了一個死鎖。

另外一個例子:

dispatch_queue_t queue  = dispatch_queue_create("test.com", DISPATCH_QUEUE_SERIAL);
    NSLog(@"之前 - %@",[NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"sync之前 - %@",[NSThread currentThread]);
        dispatch_sync(queue, ^{
            NSLog(@"sync - %@",[NSThread currentThread]);
        });
        NSLog(@"sync - 之後%@",[NSThread currentThread]);
    });
    NSLog(@"之後 - %@",[NSThread currentThread]);
複製程式碼

1.使用 DISPATCH_QUEUE_SERIAL 這個引數,建立一個序列的佇列。 2.在 dispatch_async 非同步執行,所以當前執行緒不會被阻塞,於是就有了兩條執行緒。一條當前執行緒繼續往下列印"之後 - ",而另外一條執行緒執行block中的內容列印 "sync之前 - ",這句。因為兩條是並行的所以列印的先後順序無所謂。 3.dispatch_sync同步執行,於是他把當前的執行緒阻塞,一直等到sync裡的任務執行完才會繼續往下。於是,sync就高興的把自己Block中的任務放到queue中,可誰想queue是一個序列佇列,一次執行一個任務。所以queue必須等到當前的任務執行完畢,但是queue又被阻塞了,於是就發生死鎖。

相關文章