漫談多線程:GCD(一)

導言git

多線程是程序開發中很是基礎的一個概念,你們在開發過程當中應該或多或少用過相關的東西。同時這偏偏又是一個比較棘手的概念,一切跟多線程掛鉤的東西都會變得複雜。若是使用過程當中對多線程不夠熟悉,極可能會埋下一些難以預料的坑。github

iOS中的多線程技術主要有NSThread, GCD和NSOperation。他們的封裝層次依次遞增,其中:多線程

1)NSThread封裝性最差,最偏向於底層,主要基於thread使用異步

2)GCD是基於C的API,直接使用比較方便,主要基於task使用async

3)NSOperation是基於GCD封裝的NSObject對象,對於複雜的多線程項目使用比較方便,主要基於隊列使用ide

上篇文章介紹了NSThread的用法,NSThread已經屬於古董級別的東西了,欣賞一下能夠,真正使用就不要麻煩他了。GCD是多線程中的新貴,比起NSThread更增強大,也更容易使用。因爲GCD的東西比較多,我會分好幾篇文章介紹,這篇文章主要介紹GCD中的queue相關知識。函數


dispatch_queue_toop

使用GCD以後,你能夠不用再浪費精力去關注線程,GCD會幫你管理好一切。你只須要想清楚任務的執行方法(同步仍是異步)和隊列的運行方式(串行仍是並行)便可。動畫

任務是一個比較抽象的概念,表示一段用來執行的代碼,他對應到代碼裏就是一個block或者一個函數。this

隊列分爲串行隊列和並行隊列:

1)串行隊列一次只能執行一個任務。只有一個任務執行完成以後,下一個任務才能執行,主線程就是一個串行的隊列。

2)並行隊列能夠同時執行多個任務,系統會維護一個線程池來保證並行隊列的執行。線程池會根據當前任務量自行安排線程的數量,以確保任務儘快執行。

隊列對應到代碼裏是一個dispatch_queue_t對象:

dispatch_queue_t queue;

對象就有內存。跟普通OC對象相似,咱們能夠用dispatch_retain()和dispatch_release()對其進行內存管理,當一個任務加入到一個queue中的時候,任務會retain這個queue,直到任務執行完成纔會release。

值得高興的是,iOS6以後,dispatch對象已經支持ARC,因此在ARC工程之下,咱們能夠不用擔憂他的內存,想怎麼玩就怎麼玩。

要申明一個dispatch的屬性。通常狀況下咱們只須要用strong便可。

@property (nonatomic, strong) dispatch_queue_t queue;

若是你是寫一個framework,framework的使用者的SDK有可能仍是古董級的iOS6以前。那麼你須要根據OS_OBJECT_USE_OBJC作一個判斷是使用strong仍是assign。(通常github上的優秀第三方庫都會這麼作)

#if OS_OBJECT_USE_OBJC
@property (nonatomic, strong) dispatch_queue_t queue;
#else
@property (nonatomic, assign) dispatch_queue_t queue;
#endif


async

GCD中有2個異步的API

void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work);

他們都是將一個任務提交到queue中,提交以後當即返回,不等待任務的的執行。提交以後,系統會對queue作retain操做,任務執行完成以後,queue再被release。兩個函數實際的功能是同樣的,惟一的區別在於dispatch_async接受block做爲參數,dispatch_async_f接受函數。

使用dispatch_async的時候block會被copy,在block執行完成以後block再release,因爲是系統持有block,因此不用擔憂循環引用的問題,block裏面的self不須要weak。

在dispatch_async_f中,context會做爲第一個參數傳給work函數。若是work不須要參數,context能夠傳入NULL。work參數不能傳入NULL,不然可能發生沒法預料的事。

異步是一個比較抽象的概念,簡單的說就是將任務加入到隊列中以後,當即返回,不須要等待任務的執行。語言的描述比較抽象,咱們用代碼加深一下對概念的理解:

NSLog(@"this is main queue, i want to throw a task to global queue");
dispatch_queue_t globalQueue = dispatch_queue_create("com.liancheng.global_queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(globalQueue, ^{
    // task
});
NSLog(@"this is main queue, throw task completed");

上面這段代碼,會以這樣的方式運行,紅色表示正在執行的模塊,灰色表示未執行或者已經執行完成的模塊。

1)先在main queue中執行第一個NSLog

2)dispatch_async會將block提交到globalQueue中,提交成功以後當即返回

3)main queue執行第二個NSLog

4)等global queue中block前面的任務執行完成以後,block被執行。


sync

與異步類似,GCD中同步的API也是2個

void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work);

2個API做用相同:將任務提交到queue中,任務加入queue以後不會當即返回,等待任務執行完成以後再返回。同sync相似,dispatch_sync與dispatch_sync_f惟一的區別在於dispatch_sync接收block做爲參數,block被系統持有,不須要對self使用weak。dispatch_sync_f接受函數work做爲參數,context做爲傳給work函數的第一個參數。一樣,work參數也不能傳入NULL,不然會發生沒法預料的事。

同步表示任務加入到隊列中以後不會當即返回,等待任務完成再返回。語言的描述比較抽象,咱們再次用代碼加深一下對概念的理解

NSLog(@"this is main queue, i want to throw a task to global queue");
dispatch_queue_t globalQueue = dispatch_queue_create("com.liancheng.global_queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(globalQueue, ^{
    // task
});
NSLog(@"this is main queue, throw task completed");

咱們來看看代碼的運行方式:

1)先在main queue中執行第一個NSLog

2)dispatch_sync會將block提交到global queue中,等待block的執行

3)global queue中block前面的任務執行完成以後,block執行

4)block執行完成以後,dispatch_sync返回

5)dispatch_sync以後的代碼執行

因爲dispatch_sync須要等待block被執行,這就很是容易發生死鎖。若是一個串行隊列,使用dispatch_sync提交block到本身隊列中,就會發生死鎖。

dispatch_queue_t queue = dispatch_queue_create("com.liancheng.serial_queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    // 到達串行隊列   
    dispatch_sync(queue, ^{     //發生死鎖   
    });
});

dispatch_sync的代碼執行如圖所示

dispatch_sync須要等待block執行完成,同時因爲隊列串行,block的執行須要等待前面的任務,也就是dispatch_sync執行完成。二者互相等待,永遠也不會執行完成,死鎖就這樣發生了。

從這裏看發生死鎖須要2個條件:

1)代碼運行的當前隊列是串行隊列

2)使用sync將任務加入到本身隊列中

若是queue是並行隊列,或者將任務加入到其餘隊列中,這是不會發生死鎖的。


獲取隊列

獲取主線程隊列

主線程是咱們最經常使用的線程,GCD提供了很是簡單的獲取主線程隊列的方法。

dispatch_queue_t dispatch_get_main_queue(void)方法不須要傳入參數,直接返回主線程隊列。

假設咱們要在主線程更新UI:

dispatch_async(dispatch_get_main_queue(), ^{
    [self updateUI];
});

執行加入到主線程隊列的block,App會調用dispatch_main(), NSApplicationMain(),或者在主線程使用CFRunLoop。

獲取全局隊列

除了主線程隊列,GCD提供了幾個全局隊列,能夠直接獲取使用

dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);

dispatch_get_global_queue方法獲取的全局隊列都是並行隊列,而且隊列不能被修改,也就是說對全局隊列調用dispatch_suspend(), dispatch_resume(), dispatch_set_context()等方法無效

1)identifier: 用以標識隊列優先級,推薦用qos_class枚舉做爲參數,也可使用dispatch_queue_priority_t

2)flags: 預留字段,傳入任何非0的值均可能致使返回NULL

能夠看到dispatch_get_global_queue根據identifier參數返回相應的全局隊列。identifier推薦使用qos_class枚舉

__QOS_ENUM(qos_class, unsigned int,
    QOS_CLASS_USER_INTERACTIVE
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x21,
    QOS_CLASS_USER_INITIATED
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x19,
    QOS_CLASS_DEFAULT
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x15,
    QOS_CLASS_UTILITY
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x11,
    QOS_CLASS_BACKGROUND
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x09,
    QOS_CLASS_UNSPECIFIED
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x00,
);

這個枚舉與NSThread中的NSQualityOfService相似

1)QOS_CLASS_USER_INTERACTIVE: 最高優先級,交互級別。使用這個優先級會佔用幾乎全部的系統CUP和I/O帶寬,僅限用於交互的UI操做,好比處理點擊事件,繪製圖像到屏幕上,動畫等

2)QOS_CLASS_USER_INITIATED: 次高優先級,用於執行相似初始化等須要當即返回的事件

3)QOS_CLASS_DEFAULT: 默認優先級,當沒有設置優先級的時候,線程默認優先級。通常狀況下用的都是這個優先級

4)QOS_CLASS_UTILITY: 普通優先級,主要用於不須要當即返回的任務

5)QOS_CLASS_BACKGROUND: 後臺優先級,用於用戶幾乎不感知的任務。

6)QOS_CLASS_UNSPECIFIED: 未知優先級,表示服務質量信息缺失

identifier除了使用qos_class枚舉,也能夠用dispatch_queue_priority_t做爲參數。

#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
typedef long dispatch_queue_priority_t;

INT16_MINtypedef long dispatch_queue_priority_t;

dispatch_queue_priority_t對應到qos_class枚舉有:

- DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
- DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
- DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND

不少時候咱們喜歡將0或者NULL傳入做爲參數

dispatch_get_global_queue(NULL, NULL)

因爲NULL等於0,也就是DISPATCH_QUEUE_PRIORITY_DEFAULT,因此返回的是默認優先級。


建立隊列

當沒法獲取到理想的隊列時,咱們能夠本身建立隊列。

dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);

若是未使用ARC,dispatch_queue_create建立的queue在使用結束以後須要調用dispatch_release。

1)label: 隊列的名稱,調試的時候能夠區分其餘的隊列

2)attr: 隊列的屬性,dispatch_queue_attr_t類型。用以標識隊列串行,並行,以及優先級等信息

attr參數有三種傳值方式:

// 串行
#define DISPATCH_QUEUE_SERIAL NULL
// 並行
#define DISPATCH_QUEUE_CONCURRENT \
        DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t, \
        _dispatch_queue_attr_concurrent)       
// 自定義屬性值
dispatch_queue_attr_t dispatch_queue_attr_make_with_qos_class(dispatch_queue_attr_t attr, dispatch_qos_class_t qos_class, int relative_priority);

DISPATCH_QUEUE_SERIAL或者NULL,表示建立串行隊列,優先級爲目標隊列優先級。DISPATCH_QUEUE_CONCURRENT表示建立並行隊列,優先級也爲目標隊列優先級。

dispatch_queue_attr_make_with_qos_class函數能夠建立帶有優先級的dispatch_queue_attr_t對象。經過這個對象能夠自定義queue的優先級。

1)attr: 傳入DISPATCH_QUEUE_SERIAL、NULL或者DISPATCH_QUEUE_CONCURRENT,表示串行或者並行

2)qos_class: 傳入qos_class枚舉,表示優先級級別

3)relative_priority: 相對於qos_class的相對優先級,qos_class用於區分大的優先級級別,relative_priority表示大級別下的小級別。relative_priority必須大於QOS_MIN_RELATIVE_PRIORITY小於0,不然將返回NULL。從GCD源碼中能夠查到QOS_MIN_RELATIVE_PRIORITY等於-15

使用dispatch_queue_attr_make_with_qos_class建立隊列時,須要注意,非法的參數可能致使dispatch_queue_attr_make_with_qos_class返回NULL,dispatch_queue_create傳入NULL會建立出串行隊列。寫代碼過程當中須要確保這是不是預期的結果。


設置目標隊列

除了經過dispatch_queue_attr_make_with_qos_class設置隊列的優先級以外,也可使用設置目標隊列的方法,設置隊列的優先級。當隊列建立時未設置優先級,隊列將繼承目標隊列的優先級。(不過通常狀況下仍是推薦使用dispatch_queue_attr_make_with_qos_class設置隊列的優先級)

void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);

調用dispatch_set_target_queue會retain新目標隊列queue,release原有目標隊列。設置目標隊列以後,block將會在目標隊列中執行。注意:當目標隊列串行時,任何在目標隊列中執行的block都會串行執行,不管原隊列是否串行。

假設有隊列A、B是並行隊列,C爲串行隊列。A,B的目標隊列均設置爲C,那麼A、B、C中的block在設置目標隊列以後最終都會串行執行。

例:隊列1並行,隊列2串行

dispatch_queue_t queue1 = dispatch_queue_create("com.company.queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("com.company.queue2", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue1, ^{    // block1
    for (int i = 0; i < 5; i ++) {
        NSLog(@"+++++");
    }
});
dispatch_async(queue1, ^{ // block2
    for (int i = 0; i < 5; i ++) {
        NSLog(@"=====");
    }
});
dispatch_async(queue2, ^{    // block3
    for (int i = 0; i < 5; i ++) {
        NSLog(@"----");
    }
});

運行一下可知block1,block2,block3並行執行

2016-02-25 15:05:20.024 TGCD[1940:99120] +++++
2016-02-25 15:05:20.024 TGCD[1940:99122] =====
2016-02-25 15:05:20.024 TGCD[1940:99121] ----
2016-02-25 15:05:20.025 TGCD[1940:99120] +++++
2016-02-25 15:05:20.025 TGCD[1940:99121] ----
2016-02-25 15:05:20.025 TGCD[1940:99122] =====
2016-02-25 15:05:20.025 TGCD[1940:99120] +++++
2016-02-25 15:05:20.025 TGCD[1940:99121] ----
2016-02-25 15:05:20.025 TGCD[1940:99122] =====
2016-02-25 15:05:20.025 TGCD[1940:99120] +++++
2016-02-25 15:05:20.025 TGCD[1940:99121] ----
2016-02-25 15:05:20.025 TGCD[1940:99122] =====
2016-02-25 15:05:20.025 TGCD[1940:99120] +++++
2016-02-25 15:05:20.025 TGCD[1940:99121] ----
2016-02-25 15:05:20.025 TGCD[1940:99122] =====

若是將隊列1的目標隊列設置爲隊列2,會發生什麼狀況呢?

dispatch_queue_t queue1 = dispatch_queue_create("com.company.queue1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("com.company.queue2", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queue1, queue2);
dispatch_async(queue1, ^{
    for (int i = 0; i < 5; i ++) {
        NSLog(@"+++++");
    }
});
dispatch_async(queue1, ^{
    for (int i = 0; i < 5; i ++) {
        NSLog(@"=====");
    }
});
dispatch_async(queue2, ^{
    for (int i = 0; i < 5; i ++) {
        NSLog(@"----");
    }
});

block1,block2,block3變爲了串行

2016-02-25 15:06:57.215 TGCD[1974:100675] +++++
2016-02-25 15:06:57.215 TGCD[1974:100675] +++++
2016-02-25 15:06:57.215 TGCD[1974:100675] +++++
2016-02-25 15:06:57.215 TGCD[1974:100675] +++++
2016-02-25 15:06:57.216 TGCD[1974:100675] +++++
2016-02-25 15:06:57.216 TGCD[1974:100675] =====
2016-02-25 15:06:57.216 TGCD[1974:100675] =====
2016-02-25 15:06:57.216 TGCD[1974:100675] =====
2016-02-25 15:06:57.216 TGCD[1974:100675] =====
2016-02-25 15:06:57.216 TGCD[1974:100675] =====
2016-02-25 15:06:57.216 TGCD[1974:100675] ----
2016-02-25 15:06:57.216 TGCD[1974:100675] ----
2016-02-25 15:06:57.216 TGCD[1974:100675] ----
2016-02-25 15:06:57.217 TGCD[1974:100675] ----
2016-02-25 15:06:57.217 TGCD[1974:100675] ----

注意不要循環設置目標隊列,如A的目標隊列爲B,B的目標隊列爲A。這將會致使沒法預知的錯誤


延時

GCD中有2個延時的API

dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *context, dispatch_function_t work);

必定時間以後將block加入到queue中。when用於表示時間,若是傳入DISPATCH_TIME_NOW會等同於dispatch_async。另外不容許傳入DISPATCH_TIME_FOREVER,這會永遠阻塞線程。

通前面其餘方法相似。dispatch_after接收block做爲參數,系統持有block,block中self不須要weak。dispatch_after_f接收work函數做爲參數,context做爲work函數的第一個參數。

須要注意的是這裏的延時是不精確的,由於加入隊列不必定會當即執行。延時1s可能會1.5s甚至2s以後纔會執行。


dispatch_barrier

在並行隊列中,有的時候咱們須要讓某個任務單獨執行,也就是他執行的時候不容許其餘任務執行。這時候dispatch_barrier就派上了用場。

使用dispatch_barrier將任務加入到並行隊列以後,任務會在前面任務所有執行完成以後執行,任務執行過程當中,其餘任務沒法執行,直到barrier任務執行完成。

dispatch_barrier在GCD中有4個API

void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_async_f(dispatch_queue_t queue, void *context, dispatch_function_t work);
void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_sync_f(dispatch_queue_t queue, void *context, dispatch_function_t work);

若是API在串行隊列中調用,將等同於dispatch_async、dispatch_async_f、dispatch_sync、dispatch_sync_f,不會有任何影響。

dispatch_barrier最典型的使用場景是讀寫問題,NSMutableDictionary在多個線程中若是同時寫入,或者一個線程寫入一個線程讀取,會發生沒法預料的錯誤。可是他能夠在多個線程中同時讀取。若是多個線程同時使用同一個NSMutableDictionary。怎樣才能保護NSMutableDictionary不發生意外呢?

- (void)setObject:(id)anObject forKey:(id
)aKey{
    dispatch_barrier_async(self.concurrentQueue, ^{
        [self.mutableDictionary setObject:anObject forKey:aKey];
    });
}
- (id)objectForKey:(id)aKey{
    __block id object = nil;    dispatch_sync(self.concurrentQueue, ^{
        object = [self.mutableDictionary objectForKey:aKey];
    });    return  object;
}

當NSMutableDictionary寫入的時候,咱們使用dispatch_barrier_async,讓其單獨執行寫入操做,不容許其餘寫入操做或者讀取操做同時執行。當讀取的時候,咱們只須要直接使用dispatch_sync,讓其正常讀取便可。這樣就能夠保證寫入時不被打擾,讀取時能夠多個線程同時進行


set_specific & get_specific

有時候咱們須要將某些東西關聯到隊列上,好比咱們想在某個隊列上存一個東西,或者咱們想區分2個隊列。GCD提供了dispatch_queue_set_specific方法,經過key,將context關聯到queue上

void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor);

1)queue:須要關聯的queue,不容許傳入NULL

2)key:惟一的關鍵字

3)context:要關聯的內容,能夠爲NULL

4)destructor:釋放context的函數,當新的context被設置時,destructor會被調用

有存就有取,將context關聯到queue上以後,能夠經過dispatch_queue_get_specific或者dispatch_get_specific方法將值取出來。

void *dispatch_queue_get_specific(dispatch_queue_t queue, const void *key);
void *dispatch_get_specific(const void *key);

1)dispatch_queue_get_specific: 根據queue和key取出context,queue參數不能傳入全局隊列

2)dispatch_get_specific: 根據惟一的key取出當前queue的context。若是當前queue沒有key對應的context,則去queue的target queue取,取不着返回NULL,若是對全局隊列取,也會返回NULL

iOS 6以後dispatch_get_current_queue()被廢棄(廢棄的緣由這裏很少解釋,若是想了解能夠看這裏),若是咱們須要區分不一樣的queue,可使用set_specific方法。根據對應的key是否有值來區分。


END

queue相關的內容就介紹到這裏,GCD的東西挺多,其餘東西以後若是有時間我會慢慢介紹,敬請期待

相關文章
相關標籤/搜索