GCD

1.隊列git

最大的分類有兩種:串行和並行,對應的建立方法以下github

// 串行
dispatch_queue_t synQueue = dispatch_queue_create("syn", DISPATCH_QUEUE_SERIAL);
// 並行
dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);

上面的這種建立方式是自定義隊列,第一個參數是隊列標識,第二個參數是指定是哪一種隊列。面試

系統有自己的隊列,好比主隊列和全局隊列數組

// 主隊列
dispatch_get_main_queue();
// 全局隊列
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

主隊列是串行隊列,這也是爲何咱們網絡請求通常不在主隊列發起,由於這樣在請求返回前會阻塞當前隊列,沒法進行其餘操做;全局隊列是並行隊列,第一個參數是優先級,第二個參數是給蘋果預留的,通常爲0或NULL。網絡

因此通常面試問GCD有幾種隊列,通常是主隊列、全局隊列和自定義隊列。多線程

 

2.執行方法併發

串行執行方法是app

dispatch_sync(syncQueue, ^{});

並行執行方法是異步

dispatch_async(asyncQueue, ^{});

上面的寫法是,串行隊列調用串行方法,並行隊列調用並行方法,那若是交叉過來,串行隊列調用並行方法,並行隊列調用串行方法,執行順序會是如何呢?若是一個隊列前後調用串行和並行方法,又是怎樣執行的?async

測試代碼和過程我就不寫了,這裏先寫本身測試後的結論:

 並行隊列並行執行,隨意輸出,不阻塞主線程

 串行隊列串行執行,順序輸出,阻塞主線程

 並行隊列串行執行,順序輸出,阻塞主線程

 串行隊列並行執行,順序輸出,不阻塞主線程

Tip:串行和並行就像是單車道和多車道,單車道再運行多輛行駛,也只能每次經過一輛,多車道能夠多輛行駛,也能夠排隊行駛。

 

3.barrier(屏障、柵欄)

並行隊列並行執行的時候,全部任務都是隨時執行的,但若是其中一個任務很重要,需求中它將會影響後面隊列中的任務,該如何實現?GCD中有一個功能,這個功能相似於建立一個屏障、柵欄區域來隔開先後的任務,前面的任務執行完後,再執行柵欄區域的任務,才能執行後面的任務。

    dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(asyncQueue, ^{
        NSLog(@"1");
    });
    
    dispatch_async(asyncQueue, ^{
        NSLog(@"2");
    });
    
    dispatch_barrier_async(asyncQueue, ^{
        NSLog(@"barrier");
    });
    
    dispatch_async(asyncQueue, ^{
        NSLog(@"3");
    });
    
    dispatch_async(asyncQueue, ^{
        NSLog(@"4");
    });

輸出結果是1,2,barrier,3,4,也有可能會是2,1,barrier,4,3,但barrier必定會在前兩個任務執行完以後,後兩個任務執行以前執行的。

 

4.GCD和線程同步

若是多個線程訪問同一處代碼,那麼可能會出現問題,好比對同一個值的get和set方法,多線程的時候,因爲執行的時機是隨時的,因此咱們有可能訪問get方法獲取到的不是最新的值。一般是使用鎖來實現同步機制,比較經常使用是@synchronized或者NSLock及其子類來加鎖。

- (void)lockMethod {
    @synchronized(self) {
        //do something
    }
}

以上這種寫法是根據給的對象,自動建立一個鎖,等到block總的代碼執行完,就釋放了鎖。可是以上的這個例子代碼裏,因爲鎖的對象是self,@synchronized的做用是保證此時沒有其餘線程對self對象進行訪問,這樣若是咱們在訪問加鎖程序的同時,就不能訪問其餘無關的代碼了,因此濫用@synchronized會下降代碼效率。

NSLock及其子類與@synchronized有一種缺陷,在極其極端的狀況下,同步塊會致使死鎖,另外,效率也不見得很高。

替代方案是使用GCD,示例代碼以下:

dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);

- (NSString *)stringA {
    __block NSString* A;
    dispatch_async(asyncQueue, ^{
        A = _localA;
    });
    return A;
}

- (void)setStringA:(NSString *)A {
    dispatch_barrier_async(asyncQueue, ^{
        _localA = A;
    });
}

上面的示例裏,全部的讀取A的操做,都被barrier屏蔽住,必須等賦值完後才能讀取,此時讀取到的A是最新的值。

 

5.任務組group

GCD能夠把任務分組,調用者會在回調函數中收到一組任務結束的通知。

任務組的建立:

dispatch_group_t group = dispatch_group_create();

一般的調用方法:

dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, asyncQueue, ^{
    NSLog(@"1");
});

dispatch_group_async(group, asyncQueue, ^{
    NSLog(@"2");
});

dispatch_group_async(group, asyncQueue, ^{
    NSLog(@"3");
});

dispatch_group_notify(group, asyncQueue, ^{
    NSLog(@"end");
});

等效於下面的這種方式,但區別是在任務完成前會阻塞:

dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);

dispatch_async(asyncQueue, ^{
    NSLog(@"1");
    dispatch_group_leave(group);
});

dispatch_group_enter(group);

dispatch_async(asyncQueue, ^{
    NSLog(@"2");
    
    dispatch_group_leave(group);
});

dispatch_group_enter(group);

dispatch_async(asyncQueue, ^{
    NSLog(@"3");
    
    dispatch_group_leave(group);
});

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

NSLog(@"end");

dispatch_group_enter(group)和dispatch_group_leave(group)必須成對出現,表示一個任務進入組、離開組,相似於引用計數的retain和release。dispatch_group_wait是用來阻塞線程的,第二個參數是時間參數,表示要阻塞多久,咱們用DISPATCH_TIME_FOREVER代表一直等,等到任務組都執行完才能向下執行其餘的。若是在調用enter以後,沒有對應的leave,那麼這一任務永遠執行不完,會由dispatch_group_wait一直阻塞着。

但這並非說dispatch_group_enter(group)和dispatch_group_leave(group)寫法太繁瑣,沒啥用。好比下面這段代碼

dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, asyncQueue, ^{
    NSLog(@"1");
    dispatch_async(asyncQueue, ^{
        sleep(3);
        NSLog(@"1-1");
    });
});
dispatch_group_async(group, asyncQueue, ^{
    NSLog(@"2");
});
dispatch_group_async(group, asyncQueue, ^{
    NSLog(@"3");
});

dispatch_group_notify(group, asyncQueue, ^{
    NSLog(@"notify");
});

NSLog(@"end");

結果:

2016-12-01 22:52:09.189  3
2016-12-01 22:52:09.189  2
2016-12-01 22:52:09.189  1
2016-12-01 22:52:09.189  end
2016-12-01 22:52:09.189  notify
2016-12-01 22:52:12.263  1-1

從結果能夠看到,group沒有處理任務1裏面的異步子任務,由於queue中的block是執行到末尾就返回的。若是要同步到其中的子任務,須要這樣改寫

dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
dispatch_async(asyncQueue, ^{
    NSLog(@"1");
    
    dispatch_async(asyncQueue, ^{
        sleep(3);
        NSLog(@"1-1");
        dispatch_group_leave(group);
    });
    
});

dispatch_group_enter(group);
dispatch_async(asyncQueue, ^{
    NSLog(@"2");
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(asyncQueue, ^{
    NSLog(@"3");
    dispatch_group_leave(group);
});

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"end");

結果:
2016-12-01 23:00:12.279  2
2016-12-01 23:00:12.279  3
2016-12-01 23:00:12.279  1
2016-12-01 23:00:15.350  1-1
2016-12-01 23:00:15.351  end

關鍵是把dispatch_group_enter和dispatch_group_leave放到合適的地方去。

思考:若是是一個任務組,執行兩個並行隊列的全部任務,會是如何?

 

6.循環執行

若是咱們碰上須要循環執行某些任務,好比遍歷一個數組作操做,又不想阻塞線程,該如何作呢?

用for循環這麼作:

dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);

for (id obj in array) {
    dispatch_async(asyncQueue, ^{
        // do something
    });
}

但若是再加個需求,在遍歷操做執行完後,才執行下一個任務,顯然上面的這個方法不合適,用串行又顯得不夠好。如今咱們能夠參考上面的group來實現:

dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();

for (id obj in array) {
    
    dispatch_group_async(group, asyncQueue, ^{
        // do something
    });
    
    dispatch_group_notify(group, asyncQueue, ^{
       // after end do something
    });
}

幸虧,對於循環,GCD提供了一個dispatch_apply函數來實現:

dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);

dispatch_apply(10, asyncQueue, ^(size_t index) {
    NSLog(@"index %ld", index);
});
NSLog(@"end");

結果:
index 1
index 0
index 3
index 2
index 4
index 5
index 6
index 7
index 8
index 9
end

可是dispatch_apply是同步方法,會阻塞線程,因此不要在主線程調用這個方法,並且index不是按順序輸出調用的。因而是對上面的代碼作一下調整:

dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(asyncQueue, ^(){
    dispatch_apply(10, asyncQueue, ^(size_t index){
        NSLog(@"index %ld", index);
    });
    NSLog(@"end");
});

 

7.單例

+ (id)shareInstance {
    static MyClass *myShareInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        myShareInstance = [[MyClass alloc] init];
    });
    return myShareInstance;
}

 

8.延時delay

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
    // do something
});

示例代碼是延遲5秒。GCD的延遲方法比起performSelector的延遲方法好處是,代碼集中不分散,不用另外在寫執行方法,並且傳參數沒有限制,performSelector的參數必須是id類型。

 

9.優先級

調用全局隊列的時候,有個參數是優先級,但通常是默認優先級DISPATCH_QUEUE_PRIORITY_DEFAULT,一共有四個優先級,按順序是:高、默認、低、後臺。高優先級會先執行,但注意,在極端的狀況下會出現優先級反轉的狀況,低優先級的任務佔有資源致使高優先級任務沒法執行。

 

10.信號量

對於多個線程訪問同個資源,GCD還提供是一種解決方法,就是信號量dispatch_semaphore

dispatch_queue_t asynQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);
 
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
for (int i = 0; i < 20; i++)
{
    dispatch_async(asynQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"semaphore %@------ %i", semaphore, i);
        sleep(2);
        dispatch_semaphore_signal(semaphore);
    });
}

 

dispatch_semaphore_create(2)建立了一個總量爲2的信號量;

dispatch_semaphore_wait是等待信號,並讓信號量-1,若是獲取到的信號量是0,那麼根據設置的超時時間進行等待,例子裏設置的超時時間是一直;

dispatch_semaphore_signal是發送信號,並讓信號量+1;

這套信號機制是否是很相似引用計數。因此,上面的這段代碼併發了20個任務,每一個任務都會有sleep,但每執行2個任務,經由dispatch_semaphore_wait減了兩次,就爲0,其餘的任務只能等sleep後dispatch_semaphore_signal加回信號量才能執行,如此反覆。

能夠用來坐線程同步,好比主線程與新線程同步(場景像請求動態域名)

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_queue_t asynQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);

dispatch_sync(asynQueue, ^{
    NSLog(@"asyn queue task");
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"asyn queue task finish and wait");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), asynQueue, ^{
        dispatch_semaphore_signal(semaphore);
    });
});

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(semaphore);
NSLog(@"main queue task");

結果
2016-11-30 23:44:59.044  asyn queue task
2016-11-30 23:44:59.044  asyn queue task finish and wait
2016-11-30 23:45:02.045  main queue task

note:上面用的是串行執行,若是是並行執行的話,必須先sleep主線程保證新線程的優先執行。

 

11. dispatch_set_target_queue

dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t _Nullable queue);

dispatch_set_target_queue有兩個做用,一個是繼承target_queue的優先級,固然,系統的queue的優先級是沒法用這個方法作修改的;第二個是能夠修改層級關係,好比下面這段代碼

dispatch_queue_t targetQueue = dispatch_queue_create("myTargetQueue", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t queueA = dispatch_queue_create("myQueueA", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queueB = dispatch_queue_create("myQueueB", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queueC = dispatch_queue_create("myQueueC", DISPATCH_QUEUE_SERIAL);

dispatch_set_target_queue(queueA, targetQueue);
dispatch_set_target_queue(queueB, targetQueue);
dispatch_set_target_queue(queueC, targetQueue);

dispatch_async(queueA, ^{
    NSLog(@"A-1");
});
dispatch_async(queueB, ^{
    NSLog(@"B-1");
});
dispatch_async(queueC, ^{
    NSLog(@"C-1");
});
dispatch_async(queueB, ^{
    NSLog(@"B-2");
});
dispatch_async(queueA, ^{
    NSLog(@"A-2");
});
dispatch_async(queueC, ^{
    NSLog(@"C-2");
});

結果:
2016-12-01 22:14:15.247  A-1
2016-12-01 22:14:15.248  A-2
2016-12-01 22:14:15.248  B-1
2016-12-01 22:14:15.248  B-2
2016-12-01 22:14:15.249  C-1
2016-12-01 22:14:15.249  C-2

原本每一個串行隊列的調用都是異步的,但通過dispatch_set_target_queue指定到目標的串行隊列後,執行順序就有了前後,每一個queue會先執行完其中全部任務,以後下一個queue才能執行,至關於按照queue的調用順序,把這些queue排了個序。

 

12. dispatch_suspend和dispatch_resume

dispatch_queue_t asyncQueue = dispatch_queue_create("asyn", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(asyncQueue, ^{
    NSLog(@"1");
    sleep(3);
    dispatch_resume(asyncQueue);
});
dispatch_suspend(asyncQueue);
dispatch_async(asyncQueue, ^{
    NSLog(@"2");
});
結果:
2016-12-01 23:29:41.170  1
2016-12-01 23:29:44.245  2

dispatch_suspend和dispatch_resume也是成對出現,不然會出現崩潰。

一段很神奇的代碼

dispatch_queue_t myQueue = dispatch_queue_create("123456", DISPATCH_QUEUE_SERIAL);
dispatch_async(myQueue, ^{
    NSLog(@"1");
    sleep(3);
    dispatch_resume(myQueue);
});
NSLog(@"1.5");// 最關鍵的一句,若是不加這個,1和2是沒法輸出的
dispatch_suspend(myQueue);
dispatch_async(myQueue, ^{
    NSLog(@"2");
});
NSLog(@"3");

我的猜想,加入queue的block任務,不會立馬執行,會在稍後判斷是否有任務的加入,或者其餘的針對queue操做,若是沒有,才執行block任務。因此上面的1.5的做用是使得判斷沒有針對queue的操做,因此執行block的任務。註釋掉1.5,會使得block還沒被執行,就被掛起。

 

13.dispatch_source_t timer

dispatch_source_t timer能夠作爲定時器來使用

@property (nonatomic, strong) dispatch_source_t timer;

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
self.timer = timer;
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    NSLog(@"haha");
});
dispatch_resume(timer);
dispatch_source_set_timer(dispatch_source_t source, dispatch_time_t start, uint64_t interval, uint64_t leeway);

dispatch_source_set_timer的第三個參數是指定間隔時間,第四個參數是指定能夠有多大偏差。

若是是指定時間啓動,那麼須要設置第二個參數dispatch_time_t start

dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 1.0*NSEC_PER_SEC);

咱們設置了執行的定時任務,也能夠取消,咱們把上面的代碼改寫一下

@property (nonatomic, strong) dispatch_source_t timer;

__block int i = 0;
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
self.timer = timer;
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    NSLog(@"haha");
    i++;
    if (i == 3) {
        dispatch_source_cancel(timer);
    }
    
});
dispatch_resume(timer);
    
dispatch_source_set_cancel_handler(timer, ^{
    NSLog(@"cancel");
});

結果:
2016-12-04 15:27:41.080  haha
2016-12-04 15:27:44.144  haha
2016-12-04 15:27:47.121  haha
2016-12-04 15:27:47.121  cancel

NSTimer是咱們經常使用的定時器的類,可是它有兩個問題,一個是會持有target和userinfo,一個是計時不是太精準,由於它是跟着runloop走的。MSWeakTimer是一個處理定時器timer循環引用的,它就是使用了dispatch_source_t timer。

 

最後一些注意

執行異步方法時,是須要拷貝block的,因此若拷貝所須要的時間超過執行的時間,顯得效率下降,異步的效果得不償失。

不要調用dispatch_get_current_queue(),由於若是當前隊列正在執行同步方法,會引發死鎖。所幸這個方法蘋果已經廢棄了。

在非ARC項目中,dispatch_queue_create生成的線程須要手動dispatch_release,即手動管理dispatch_queue_t內存。

線程直到執行完全部提交到其中處理的block執行完,纔會釋放,在其中任何地方調用dispatch_release,都不會當即被釋放。

If your app isn’t using ARC, you should call dispatch_release on a dispatch queue when it’s no longer needed. 
Any pending blocks submitted to a queue hold a reference to that queue, so the queue is not deallocated until all pending blocks have completed.
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息