iOS多線程之GCD、GCD處理多任務的網(wǎng)絡請求、多讀單寫

在軟件開發(fā)中使用多線程可以大大地提高用戶體驗,提高效率。Grand Central Dispatch(CGD)則是C語言的一套多線程開發(fā)框架,相比NSThread和NSOperation,GCD更加高效,并且線程由系統(tǒng)管理,會自動運行多核運算。因為這些優(yōu)勢,GCD是Apple推薦給開發(fā)者使用的首選多線程解決方案。

1、GCD的調度機制

GCD框架中一個很重要的概念是調度隊列,我們對線程的操作實際上是由調度隊列完成的。我們只需要將要執(zhí)行的任務添加到合適的隊列中即可。在GCD框架中,有如下三種類型的調度隊列。

1.1主隊列

其中的任務在主線程中執(zhí)行,因為其會阻塞主線程,所以是一個串行的隊列。可以通過下面的方法得到:

dispatch_get_main_queue();

1.2全局并行隊列

隊列中任務的執(zhí)行嚴格按照先進先出的模式進行。如果是串行的隊列,則當一個任務結束后,才會開啟另一個任務,如果是并行隊列,則任務的開啟順序和添加順序是一致的。系統(tǒng)為iOS應用自動創(chuàng)建了4個全局共享的并發(fā)隊列。使用下面的函數(shù)獲得:

dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>);

上面函數(shù)的第一個參數(shù)是這個隊列的ID,系統(tǒng)的4個全局隊列默認的優(yōu)先級不同,這個參數(shù)可填寫的定義如下:

#define DISPATCH_QUEUE_PRIORITY_HIGH 2 //優(yōu)先級別最高的全局隊列
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0//優(yōu)先級別中等的全局隊列
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)//優(yōu)先級別較低的全局隊列
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN//后臺的全局隊列,優(yōu)先級別最低

這個函數(shù)的第二個參數(shù)是一個預留參數(shù),我們可以傳NULL.

1.3自定義隊列

上面的兩種隊列都是系統(tǒng)為我們創(chuàng)建好的,我們只需要獲取到他們,添加任務即可。當然我們也可以創(chuàng)建自己的隊列,包含串行和并行的。使用如下方法來創(chuàng)建:

dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);

其中第一個參數(shù)是這個隊列的名字,第二個參數(shù)決定創(chuàng)建的是串行還是并行隊列。填寫DISPATCH_QUEUE_SERIAL或NULL創(chuàng)建串行隊列,填寫DISPATCH_QUEUE_CONCURRENT創(chuàng)建并行隊列。

2、添加任務到調度隊列中

使用dispatch_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)函數(shù)或者dispatch_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)函數(shù)來同步或異步的執(zhí)行任務。示例如下:

- (void)creatGCDQueue {
    //創(chuàng)建一個串行的隊列
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    //向隊列中添加同步任務1
    dispatch_sync(queue, ^{
        NSLog(@"%@:task1",[NSThread currentThread]);
    });
    //向隊列中添加異步任務2
    dispatch_async(queue, ^{
        NSLog(@"%@:task2",[NSThread currentThread]);
    });
    
}

//打印信息:


image.png

上面的代碼創(chuàng)建了一個串行的自定義隊列,并且向隊列中添加了一個同步的任務和一個異步的任務。需要注意,這里的同步和異步指的是針對當前代碼運行所在的線程而言的。
從打印信息可以看出,同步的任務是在主線程中執(zhí)行,異步的任務是在單獨的線程中執(zhí)行,由于我們創(chuàng)建的調度隊列是串行的,因此先開啟了任務1,后開啟了任務2.

只有當調度隊列是并行,而且向隊列中添加的任務也是異步的時候,多任務才會實現(xiàn)并行異步執(zhí)行。

實現(xiàn)如下:

- (void)creatGCDQueue {
    //創(chuàng)建一個并行的隊列
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    //向隊列中添加異步任務1
    dispatch_async(queue, ^{
        for (int i = 0; i < 15; i ++) {
            NSLog(@"%@ = %d:task1",[NSThread currentThread],i);
        }
    });
    //向隊列中添加異步任務2
    dispatch_async(queue, ^{
        for (int i = 0; i < 15; i ++) {
            NSLog(@"%@ = %d:task2",[NSThread currentThread],i);
        }    });
    
}

3、使用隊列組

通過前面的學習,我們現(xiàn)在已經(jīng)可以運用隊列多線程執(zhí)行任務了,但是GCD的強大之處遠遠不止如此??聪旅娴睦印?br> 如果有3個任務A、B、C,其中A與B是沒有關系的,他們可以并行執(zhí)行,C必須是A、B都結束之后才能執(zhí)行,當然,實現(xiàn)這樣的邏輯并不困難,使用KVO就可以實現(xiàn),但是如果使用隊列處理這樣的邏輯,則代碼會更加清晰簡單。
可以使用dispatch_group_create()創(chuàng)建一個隊列組,使用如下函數(shù)將隊列添加到隊列組中:

void dispatch_group_async(dispatch_group_t group,
    dispatch_queue_t queue,
    dispatch_block_t block);

隊列中的隊列是異步執(zhí)行的,示例如下:

- (void)creatGCDGroup {
    //創(chuàng)建一個隊列組
    dispatch_group_t group = dispatch_group_create();
    //創(chuàng)建一個異步隊列
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    //添加任務
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 10; i ++) {
            NSLog(@"%@ = %d:task1",[NSThread currentThread],i);
        }
    });
    
    //添加任務
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 10; i ++) {
            NSLog(@"%@ = %d:task2",[NSThread currentThread],i);
        }
    });
    
    //阻塞線程,直到前面的隊列任務執(zhí)行完成
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    for (int i = 0; i < 10; i ++) {
        NSLog(@"%@ = %d:over",[NSThread currentThread],i);
    }
}

打印結果如下:


image.png

以上代碼完美的實現(xiàn)了我們的任務依賴需求,可以看出GCD的強大了吧,復雜的任務邏輯關系因為GCD變得十分清晰簡單。

4、GCD對循環(huán)任務的處理

說到循環(huán),除了常規(guī)的while循環(huán),for循環(huán)外,for-in也是開發(fā)中常用的一種循環(huán)方式。for-in循環(huán)通常來進行數(shù)組或字典的遍歷,這種遍歷通常不關心循環(huán)執(zhí)行的順序。使用GCD,配合設備的多核運算技術,我們可以將這種循環(huán)遍歷的性能提升到極致,示例如下:

- (void)creatGCDApply {
    dispatch_apply(20, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t i) {
        NSLog(@"%@:%zu",[NSThread currentThread],i);
    });
}

打印信息如下:


image.png

從打印信息可以看出,循環(huán)是由多個不同的線程完成的,比如我們的設備是8核的CPU。因此每個線程單獨在一個核執(zhí)行,這將循環(huán)的運行效率提升到了極致。大大提高了運行速率。

5、GCD中的消息與信號

5.1Dispatch Source

在GCD框架中提供了dispatch_source_t類型的對象,dispatch_source_t類型的對象可以用來傳遞和接收某個消息。在任一線程上調用它的一個函數(shù) dispatch_source_merge_data 后,會執(zhí)行 Dispatch Source 事先定義好的句柄(可以把句柄簡單理解為一個 block )。
這個過程叫 Custom event ,用戶事件。是 dispatch source 支持處理的一種事件。簡單地說,這種事件是由你調用 dispatch_source_merge_data 函數(shù)來向自己發(fā)出的信號。
示例如下:

- (void)creatGCDSource {
    //創(chuàng)建一個數(shù)據(jù)對象,DISPATCH_SOURCE_TYPE_DATA_ADD的含義表示當數(shù)據(jù)變化時相加
    dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
    //設置響應分派源事件的block,在分派源指定的隊列上運行
    dispatch_source_set_event_handler(source, ^{
        
        NSLog(@"%lu:sec",dispatch_source_get_data(source));//得到分派源的數(shù)據(jù)
        dispatch_async(dispatch_get_main_queue(), ^{
            //更新UI
        });
        
    });
    //啟動
    dispatch_resume(source);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       
        //網(wǎng)絡請求
        //向分派源發(fā)送事件,需要注意的是,不可以傳遞0值(事件不會被觸發(fā)),同樣也不可以傳遞負數(shù)。
        dispatch_source_merge_data(source, 1);
    });

}

注意:DISPATCH_SOURCE_TYPE_DATA_ADD是將所有觸發(fā)結果相加,最后統(tǒng)一執(zhí)行響應,但是加入sleepForTimeInterval后,如果interval的時間越長,則每次觸發(fā)都會響應,但是如果interval的時間很短,則會將觸發(fā)后的結果相加后統(tǒng)一觸發(fā)。這在更新UI時很有用,比如更新進度條時,沒必要每次觸發(fā)都響應,因為更新時還有其他的用戶操作(用戶輸入,觸碰等),所以可以統(tǒng)一觸發(fā)

比如我們寫一個進度條的示例:

- (void)creatGCDSource {
    //1、指定DISPATCH_SOURCE_TYPE_DATA_ADD,做成Dispatch Source(分派源)。設定Main Dispatch Queue 為追加處理的Dispatch Queue
       dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
       
       __block NSUInteger totalComplete = 0;
       
       dispatch_source_set_event_handler(source, ^{
           
           //當處理事件被最終執(zhí)行時,計算后的數(shù)據(jù)可以通過dispatch_source_get_data來獲取。這個數(shù)據(jù)的值在每次響應事件執(zhí)行后會被重置,所以totalComplete的值是最終累積的值。
           NSUInteger value = dispatch_source_get_data(source);
           
           totalComplete += value;
           
           NSLog(@"進度:%@", @((CGFloat)totalComplete/100));
           
           NSLog(@":large_blue_circle:線程號:%@", [NSThread currentThread]);
       });
       
       //分派源創(chuàng)建時默認處于暫停狀態(tài),在分派源分派處理程序之前必須先恢復。
       dispatch_resume(source);
       
       dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
       
       //2、恢復源后,就可以通過dispatch_source_merge_data向Dispatch Source(分派源)發(fā)送事件:
    
       dispatch_async(queue, ^{
       
           for (NSUInteger index = 0; index < 100; index++) {
       
               dispatch_source_merge_data(source, 1);
       
               NSLog(@":recycle:線程號:%@~~~~~~~~~~~~i = %ld", [NSThread currentThread], index);
       
               sleep(0.1);
           }
       });

}

5.2、信號量 singer

信號量是GCD中一個很重要的概念,他的用法與消息的傳遞有所類似,其本示例代碼如下:

- (void)creatGCDSinger {
    //創(chuàng)建一個信號,其中的參數(shù)是信號的初始值
    dispatch_semaphore_t singer = dispatch_semaphore_create(0);
    //發(fā)送信號,信號量+1
    dispatch_semaphore_signal(singer);
    //等待信號,當信號量大于0時,執(zhí)行后面的代碼,否則等待,第二個參數(shù)為等待的超時時長,下面設置的為一直等待
    dispatch_semaphore_wait(singer, DISPATCH_TIME_FOREVER);
    NSLog(@"singer");
}

注意,dispatch_semaphore_wait函數(shù)會阻塞當前線程,在主線程中要慎用。通過發(fā)送信號函數(shù):dispatch_semaphore_signal(),可以使信號量+1,每次執(zhí)行過等待信號后,信號量會-1,如此,我們可以很方便地控制不同隊列中方法的執(zhí)行流程。

5.2.1限制線程的最大并發(fā)數(shù)
- (void)creatGCDSinger {
    //創(chuàng)建一個信號,其中的參數(shù)是信號的初始值
    dispatch_semaphore_t singer = dispatch_semaphore_create(2);
    for (int i = 0; i < 15; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            //等待信號,當信號量大于0時,執(zhí)行后面的代碼,否則等待,第二個參數(shù)為等待的超時時長,下面設置的為一直等待
            dispatch_semaphore_wait(singer, DISPATCH_TIME_FOREVER);
            //doing
            sleep(1);
            //發(fā)送信號,信號量+1
            dispatch_semaphore_signal(singer);
        });
    }
}

如上述代碼可知,總共異步執(zhí)行15個任務,但是由于我們設置了值為2的信號量,每一次執(zhí)行任務的時候信號量都會先-1,而在任務結束后使信號量加1,當信號量減到0的時候,說明正在執(zhí)行的任務有2個,這個時候其它任務就會阻塞,直到有任務被完成時,這些任務才會執(zhí)行。

注意,信號量的正常的使用順序是先降低(dispatch_semaphore_wait)然后再提高(dispatch_semaphore_signal),這兩個函數(shù)通常成對使用。

5.2.2阻塞發(fā)請求的線程

有些時候,我們需要阻塞發(fā)送請求的線程,比如在多個請求回調后統(tǒng)一操作的需求,而這些請求之間并沒有順序關系,且這些接口都會另開線程進行網(wǎng)絡請求的。一般地,這種多線程完成后進行統(tǒng)一操作的需求都會使用隊列組(dispatch_group_t)來完成,但是由于是異步請求,沒等其異步回調之后,請求的線程就結束了,為此,就需要使用信號量來阻塞住發(fā)請求的線程。實現(xiàn)代碼如下:

- (void)creatGCDSinger {
    //創(chuàng)建線程組
    dispatch_group_t group = dispatch_group_create();
    //獲取隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //任務1
    dispatch_group_async(group, queue, ^{
       //請求1
        [self request1];
    });
    
    //任務2
    dispatch_group_async(group, queue, ^{
       //請求2
        [self request2];
    });
    
    //任務3
    dispatch_group_async(group, queue, ^{
       //請求3
        [self request3];
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
         NSLog(@"-------所有網(wǎng)絡請求已請求完成-------");
     });
}

- (void)request1 {
    //創(chuàng)建信號量,并設置為0,信號量本質是資源數(shù),為0表示用完,需要等待
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    //模擬網(wǎng)絡請求-異步
    //每次網(wǎng)絡請求成功或失敗后,都讓信號量+1,表示釋放當前資源,其他線程可以搶占了
    [[KNetRequestManager share] getSomeData:^{
        //網(wǎng)絡請求成功,發(fā)送信號
      dispatch_semaphore_signal(sema);
    } errorBlock:^{
        //網(wǎng)絡請求失敗,發(fā)送信號
        dispatch_semaphore_signal(sema);
    }];
    //如果信號量為0,表示沒有資源可用,便一直等待,不再往下執(zhí)行.只有當網(wǎng)絡請求成功或失敗時,才會往下走
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}

- (void)request2 {
    //創(chuàng)建信號量,并設置為0,信號量本質是資源數(shù),為0表示用完,需要等待
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    //模擬網(wǎng)絡請求-異步
    //每次網(wǎng)絡請求成功或失敗后,都讓信號量+1,表示釋放當前資源,其他線程可以搶占了
    [[KNetRequestManager share] getSomeData:^{
        //網(wǎng)絡請求成功,發(fā)送信號
      dispatch_semaphore_signal(sema);
    } errorBlock:^{
        //網(wǎng)絡請求失敗,發(fā)送信號
        dispatch_semaphore_signal(sema);
    }];
    //如果信號量為0,表示沒有資源可用,便一直等待,不再往下執(zhí)行
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}

- (void)request3 {
    //創(chuàng)建信號量,并設置為0,信號量本質是資源數(shù),為0表示用完,需要等待
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    //模擬網(wǎng)絡請求-異步
    //每次網(wǎng)絡請求成功或失敗后,都讓信號量+1,表示釋放當前資源,其他線程可以搶占了
    [[KNetRequestManager share] getSomeData:^{
        //網(wǎng)絡請求成功,發(fā)送信號
      dispatch_semaphore_signal(sema);
    } errorBlock:^{
        //網(wǎng)絡請求失敗,發(fā)送信號
        dispatch_semaphore_signal(sema);
    }];
    //如果信號量為0,表示沒有資源可用,便一直等待,不再往下執(zhí)行
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}

當然,我們也可以使用dispatch_group_enter和dispatch_group_leave來實現(xiàn)同樣的功能:

- (void)creatGCDSinger {
    //創(chuàng)建線程組
    dispatch_group_t group = dispatch_group_create();
    //創(chuàng)建一個并發(fā)隊列
    dispatch_queue_t queue = dispatch_queue_create("group.queue", DISPATCH_QUEUE_CONCURRENT);
    //任務1
     dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
       //請求1
        [self request1WithGroup:group];
    });
    
    //任務2
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
       //請求2
        [self request2WithGroup:group];
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
         NSLog(@"-------所有網(wǎng)絡請求已請求完成-------");
     });
}

- (void)request1WithGroup:(dispatch_group_t)group {
    //模擬網(wǎng)絡請求-異步
    [[KNetRequestManager share] getSomeData:^{
        //網(wǎng)絡請求成功,調用level
      dispatch_group_leave(group);
    } errorBlock:^{
         //網(wǎng)絡請求失敗,調用level
        dispatch_group_leave(group);
    }];

}

- (void)request2WithGroup:(dispatch_group_t)group
    //模擬網(wǎng)絡請求-異步
    [[KNetRequestManager share] getSomeData:^{
        //網(wǎng)絡請求成功,調用level
      dispatch_group_leave(group);
    } errorBlock:^{
         //網(wǎng)絡請求失敗,調用level
        dispatch_group_leave(group);
    }];
}
5.2.3信號量控制網(wǎng)絡請求順序
- (void)creatGCDSinger {
    //創(chuàng)建semp
    dispatch_semaphore_t semp = dispatch_semaphore_create(1);
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //任務1
    dispatch_async(queue, ^{
        //信號量-1
        dispatch_semaphore_wait(semp, DISPATCH_TIME_FOREVER);
        //模擬網(wǎng)絡請求
        //模擬網(wǎng)絡請求-異步
        //每次網(wǎng)絡請求成功或失敗后,都讓信號量+1,表示釋放當前資源,其他線程可以搶占了
        [[KNetRequestManager share] getSomeData:^{
            //網(wǎng)絡請求成功,發(fā)送信號
          dispatch_semaphore_signal(sema);
        } errorBlock:^{
            //網(wǎng)絡請求失敗,發(fā)送信號
            dispatch_semaphore_signal(sema);
        }];
    });
    //任務2
    dispatch_async(queue, ^{
        //信號量-1
        dispatch_semaphore_wait(semp, DISPATCH_TIME_FOREVER);
        //模擬網(wǎng)絡請求
        //模擬網(wǎng)絡請求-異步
        //每次網(wǎng)絡請求成功或失敗后,都讓信號量+1,表示釋放當前資源,其他線程可以搶占了
        [[KNetRequestManager share] getSomeData:^{
            //網(wǎng)絡請求成功,發(fā)送信號
          dispatch_semaphore_signal(sema);
        } errorBlock:^{
            //網(wǎng)絡請求失敗,發(fā)送信號
            dispatch_semaphore_signal(sema);
        }];
    });
}

6、隊列的掛起和開啟

在GCD框架中還提供了暫停與開始任務隊列的方法,使用下面的函數(shù)可以將隊列或隊列組暫時掛起和開啟:

//掛起隊列或隊列組
void dispatch_suspend(dispatch_object_t object);
//開啟隊列或隊列組
void dispatch_resume(dispatch_object_t object);

注意:在暫停隊列時,隊列中正在執(zhí)行的任務并不會中斷,未開啟的任務會被掛起。

7、數(shù)據(jù)存儲的線程安全問題-多度單寫

在進行多線程編程時,或許總會遇到這一類問題:數(shù)據(jù)的競爭與線程的安全。這些問題如果通過程序手動來控制,則難度將會非常大。CGD同樣為我們簡單地解決了這樣的問題。
首先,如果只是在讀取數(shù)據(jù),而不對數(shù)據(jù)做任何修改,那么我們并不需要處理安全問題,可以讓多個任務同時進行讀取??墒侨绻獙?shù)據(jù)進行寫操作,那么在同一時間,我們就必須只能有一個任務在寫,CGD中有一個方法幫我們完美地解決了這個問題,示例如下:

- (void)creatCGDReadAndWriter {
    //創(chuàng)建一個隊列
    dispatch_queue_t queue = dispatch_queue_create("oneQueue", DISPATCH_QUEUE_CONCURRENT);
    //多個任務同時執(zhí)行讀操作
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"read1:%d",i);
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"read2:%d",i);
        }
    });
    
    //執(zhí)行寫操作
    /*
     下面這個函數(shù)在加入隊列時不會執(zhí)行,會等待已經(jīng)開始的異步執(zhí)行全部完成后再執(zhí)行,并且在執(zhí)行時會阻塞其他任務
     當執(zhí)行完成后,其他任務重新進入異步執(zhí)行
     */
    dispatch_barrier_async(queue, ^{
        for (int i = 0; i < 5; i ++) {
             NSLog(@"writer:%d",i);
        }
    });
    //績效執(zhí)行異步操作
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"read3:%d",i);
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"read4:%d",i);
        }
    });
}

打印信息:


image.png

從打印信息可以看出讀操作是異步進行的,寫操作是等待當前任務結束后阻塞任務隊列獨立進行的,當寫操作結束后隊列恢復異步執(zhí)行讀操作,這正是我們需要的效果。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容