iOS 從實際出發理解多線程

前言ios


    

      多線程不少開發者多多少少相信也都有了解,之前有些東西理解的不是很透,慢慢的積累以後,這方面的東西也須要本身好好的總結一下。多線程從我剛接觸到iOS的時候就知道這玩意挺重要的,但那時也是能力有限,沒辦法很好的理解它,要是隻是查它的概念性的東西,網上一搜一大把,咱們再那樣去總結就顯得意義不大了。這篇文章從我剛開始構思着去寫的時候,就但願本身能換個角度去寫,想從實際問題出發總結多線程,那就從第三方以及本身看到的一些例子還有前段時間讀的多線程和內存管理的書中分析理解總結一下多線程。數據庫

 

這幾個概念很容易繞暈macos


 

       進程:進程就是線程的容器,你打開一個App就是打開了一個進程,QQ有QQ的進程,微信有微信的進程,一個進程能夠包含多個線程,要是把進程比喻成一條高速公路,線程就是高速路上的一條條車道,也正是由於有了這些車道,整個交通的運行效率變得更高,也正是由於有了多線程的出現,整個系統運行效率變得更高。編程

      二 線程:線程就是在進程中我麼開闢的一條條爲咱們作事的進程實體,總結的通俗一點,線程就是咱們在進程上開闢的一條條作咱們想作的事的通道。 一條線程在一個時間點上只能作一件「事」,多線程在同一時間點上,就能作多件「事」,這個理解,仍是咱們前面說的高速路的例子。微信

      一條高速路是一個進程, 一條條車道就是不一樣的線程,在過收費站的時候,這條進程上要是隻有一條線程,也就是一條高速路上只有一個車道,那你就只能排隊一輛一輛的經過,同一時間不可能有兩輛車一塊兒過去,但要是你一個進程上有多個線程,也就是高速路上有幾個車道,也就有多個窗口收費,這樣的話同一時間就徹底有可能兩輛車一塊兒交完費經過了,這樣說相信也能理解這個進程和線程的關係了。多線程

  • 同步線程:同步線程會阻塞當前的線程去執行同步線程裏面想作的「事」(任務),執行完以後纔會返回當前線程。       
  • 異步線程:異步線程不會阻塞當前的線程去執行異步線程裏面想作的「事」,由於是異步,因此它會從新開啓一個線程去作想作的「事」。 

      三 隊列:隊列就是用來管理下面說的「任務」的,它採用的是先進先出(FIFO)的原則,它衍生出來的就是下面的它的分類並行和串行隊列,一條線程上能夠有多個隊列。併發

  • 並行隊列:這個隊列裏面的任務是能夠併發(同時)執行的,因爲咱們知道,同步執行任務不會開啓新的線程,因此並行隊列同步執行任務任務只會在一條線程裏面同步執行這些任務,又因爲同步執行也就是在當前線程中作事,這個時候就須要一件一件的讓「事」(任務)作完在接着作下一個。但要是是併發隊列異步執行,就對應着開啓異步線程執行要作的「事」(任務),就會同一時間又許多的「事」被作着。
  • 串行隊列:這個隊列裏面的任務是串行也就是一件一件作的,串行同步會一件一件的等事作完再接着作下一件,要是異步的就會開啓一條新的線程串行的執行咱們的任務。

    四 任務:任務按照本身通俗一點的理解,就是提到的「事」這個概念,這個「事」就能夠理解爲任務,那這個「事」也確定是在線程上面執行的(不論是在當前線程仍是你另開啓的線程)。這個「事」你能夠選擇同步或者而是異步執行,這就衍生出了東西也就契合線程上面的同步線程和異步線程。app

  • 同步任務:不須要開啓新的線程,在當前線程執行就能夠。
  • 異步任務:你須要開闢一條新的線程去異步的執行這個任務。     

      iOS當中還有一個特殊的串行隊列-- 主隊列, 這個主隊列中運行着一條特殊的線程 -- 主線程異步

      主線程又叫UI線程,UI線程顧名思義主要的任務及時處理UI,也只有主線程有處理UI的能力,其餘的耗時間的操做咱們就放在子線程(也就是開闢線程)去執行,開線程也會佔據必定的內存的,因此不要同時開啓不少的線程。 async

      經過上面的內容解釋了多線程裏面幾個關鍵的概念的東西,要是有不理解的地方歡迎多交流,下面再給出隊列執行時候的一個運行的表格,咱們一個一個慢慢的解釋。

     

 

NSThread


 

      其實在咱們平常的開發中NSThread使用也是挺多的,具體關於它的一些咱們須要注意的地方咱們一步步的開始說,先看看它的初始化的幾個方法

/*
 初始化NSThread的類方法,具體的任務在Block中執行
 + (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
 
 利用selector方法初始化NSThread,target指selector方法從屬於的對象  selector方法也是指定的target對象的方法
 + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
 
 初始化NSThread的方法,這兩個方法和上面兩個方法的區別就是這兩個你能獲取到NSThread的對象
 具體的參數和前面解釋的參數意義都是同樣的
 切記一點:  下面兩個方法初始化的NSThread你須要手動start開啓線程
 - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);
 
 - (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
 */

 

      除了上面四個咱們提出的方法,咱們在初始化這個問題上還須要注意的還有一點,就是 NSObject (NSThreadPerformAdditions) ,爲咱們的NSObject添加的這個類別,它裏面的具體的一些方法咱們也是很經常使用的:

/*
 這個方法你執行的aSelector就是在MainThread執行的,也就是在主線程
 注意這裏的waitUntilDone這個後面的BOOL類型的參數,這個參數表示是否等待一直到aSelector這個方法執行結束
 modes是RunLoop的運行的類型這個RunLoop我也會好好在總結後面
 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
 
 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
   // equivalent to the first method with kCFRunLoopCommonModes
 
 上面的兩個方法是直接在主線程裏面運行,下面的這兩個方法是要在你初始化的thr中去運行,其餘的參數和上面解釋的同樣
 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
 
 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
   // equivalent to the first method with kCFRunLoopCommonModes
 
 - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
 */

     

      咱們在說說前面說的waitUntilDone後面的這個BOOL類型的參數,這個參數的意義有點像咱們是否同步執行aSelector這個任務!具體的看下面兩張圖的內容就一目瞭然了:

      在看看等於YES的時候結果的輸出狀況:

 

      關於NSThread咱們再說下面幾個方法的具體的含義就不在描述了,關於NSThread有什麼其餘的問題,能夠加我QQ交流:

/*
 
 設置線程沉睡到指定日期
 + (void)sleepUntilDate:(NSDate *)date;
 
 線程沉睡時間間隔,這個方法在設置啓動頁間隔的時候比較常見
 + (void)sleepForTimeInterval:(NSTimeInterval)ti;
 
 線程退出,當執行到某一個特殊狀況下的時候你能夠退出當前的線程,注意不要在主線程隨便調用
 + (void)exit;
 
 線程的優先級
 + (double)threadPriority;
 
 設置線程的優先級
 + (BOOL)setThreadPriority:(double)p;
 
 */

 

NSOperation


 

      多線程咱們還得提一下NSOperation,它可能比咱們認識中的要強大一點,NSOperation也是有不少東西能夠說的,前面的NSThread其實也是同樣,這些要是仔細說的話都能寫一篇文章出來,可能之後隨着本身接觸的愈來愈多,關於多線程這一塊的東西咱們會獨立的建立一個分類總結出去。

      首先得知道NSOperation是基於GCD封裝的,NSOperation這個類自己咱們使用的時候不躲,更多的是集中在蘋果幫咱們封裝好的NSInvocationOperation和NSBlockOperation

      你command一下NSOperation進去看看,有幾個點你仍是的瞭解一下的,主要的就是下面的幾個方法:

NSOperation * operation = [[NSOperation alloc]init];
[operation start];   //開始
[operation cancel]; //取消
[operation setCompletionBlock:^{
    //operation完成以後的操做
}];

      咱們具體的說一下咱們上面說的兩個類:NSInvocationOperation和NSBlockOperation,先看看NSInvocationOperation的初始化:

/*
 
 初始化方法 看過前面的文章以後它的target 、sel 、arg 等參數相信不難理解
 -(nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
 
 -(instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER;
 
 */

       補充: NS_DESIGNATED_INITIALIZER 指定初識化方法並非對使用者。而是對內部的現實,能夠點擊進去具體瞭解一下它!NSInvocationOperation實際上是同步執行的,所以單獨使用的話就價值不大了,它和NSOperationQueue一塊兒去使用才能實現多線程調用。這個咱們後面再具體的說

      在看看NSBlockOperation這個,它重要的方法就咱們下面的兩個        

/*
 初始化方法
 + (instancetype)blockOperationWithBlock:(void (^)(void))block;
 
 添加一個能夠執行的block到前面初始化獲得的NSBlockOperation中
 - (void)addExecutionBlock:(void (^)(void))block;
 */

      NSBlockOperation這個咱們得提一點: 它的最大的併發具體的最大併發數和運行環境也是有關係的,具體的內容咱們能夠戳戳這裏同行總結以及驗證的,咱們因爲篇幅的緣由就不在這裏累贅。

      其實只要是上面這些的話是不夠咱們平常使用的,但還有一個激活他們倆的類咱們也得說說:NSOPerationQueue 下面是關於它的大概的一個說明,都挺簡單,就不在特地寫Demo。

      

      關於NSOperation的咱們就說這麼多,下面重點說一下GCD。

  

主角GCD -- 主線程


      

      一、咱們先從主隊列,主線程開始提及,經過下面的方法咱們就能夠獲取獲得主隊列:

dispatch_queue_t mainqueue = dispatch_get_main_queue(); 

      二、咱們在主線程同步執行任務,下面是操做的結果以及打印的信息:

 

      咱們解釋一下爲何在主線程中執行同步任務會出現這個結果,咱們一步一步的梳理一下這個執行過程:

  1. 獲取到在主隊列主線程中執行了最前面的打印信息,這個沒什麼問題
  2. 開始執行dispatch_sync這個函數,主隊列是串行隊列,這個函數會把這個任務插入到主隊列的最後面(理解隊列添加任務)
  3. 主線程執行到這裏的時候就會等待插入的這個同步任務執行完以後再執行後面的操做
  4. 但因爲這個同步任務是插入到主隊列的最後面,最隊列前面的任務沒有執行完以前是不會執行這個block的(主線程在執行initMainQueue任務)
  5. 這樣就形成了一個相互等待的過程,主線程在等待block完返回,block卻在等待主線程執行它,這樣就形成了死鎖,看打印的信息你也就知道block是沒有被執行的。

      這裏咱們你可能會思考,主隊列是一個串行隊列,那咱們在主線程中添加一個串行隊列,再給串行隊列添加一個同步任務,這時候和前面主線程主隊列添加同步任務不就場景同樣了嗎?那結果呢? 咱們看看下面的打印:

 

 

      咱們按照前面的方式解釋一下這個的執行步驟:

  1. 主線程在執行主隊列中的方法initSerialQueue,到這個方法時候建立了一個串行隊列(注意不是主隊列)打印了前面的第一條信息
  2. 執行到dispatch_sync函數,這個函數給這個串行隊列中添加了一個同步任務,同步任務是會立馬執行的
  3. 主線程就直接操做執行了這個隊列中的同步任務,打印的第二條信息
  4. 主線程接着執行下面的第三條打印信息

      理解:看這個執行的過程對比前面的,你就知道了不一樣的地方就是前面是添加在了主隊列當中,但這裏有添加到主隊列,因爲是插入到主隊列的末尾,因此須要主隊列的任務都執行完才能指定到它,但主線程執行到initMainQueue這個方法的時候在等待這個方法中添加的同步任務執行完接着往下執行,但它裏面的同步任務又在等待主線程執行完在執行它,就相互等待了,但主線程執行不是主隊列裏面的同步任務的時候是不須要主線程執行完全部操做在執行這個任務的,這個任務是它添加到串行隊列的開始也是結束的任務,因爲不須要等待,就不會形成死鎖!

      上面這個問題常常會看到有人問,有許多解釋,也但願本身能把這個問題給說清楚了!

 

      三、主線程這裏咱們再提一點,就是線程間的信息簡單傳遞

      前面咱們有說到主線程又叫作UI線程,全部關於UI的事咱們都是在主線程裏面更新的,像下載數據以及數據庫的訪問等這些耗時的操做咱們是建議放在子線程裏面去作,那就會產生子線程處理完這些以後要回到主線程更行UI的問題上,這一點值得咱們好好的注意一下,但其實這一點也是咱們用的最多的,相信你們也都理解!

 

主角GCD  --  串行隊列


 

      串行隊列的概念性的東西咱們就不在這裏累贅,不論是串行隊列+同步任務仍是串行隊列+異步任務都簡單,有興趣能夠本身是這寫一下,後面分析會提到他們的具體使用的,咱們在一個稍微比前面的說的複雜一點點的問題,串行隊列+異步+同步,能夠先試着不要往下面看先分析一下下面這段代碼的執行結果是什麼?

static void * DISPATCH_QUEUE_SERIAL_IDENTIFY;

-(void)initDiapatchQueue{

        dispatch_queue_t serialQueue = dispatch_queue_create(DISPATCH_QUEUE_SERIAL_IDENTIFY, DISPATCH_QUEUE_SERIAL);
        dispatch_async(serialQueue, ^{
           
                NSLog(@"一個異步任務的內容%@",[NSThread currentThread]);
                dispatch_sync(serialQueue, ^{
                   
                        NSLog(@"一個同步任務的內容%@",[NSThread currentThread]);
                });
        });
}

 

不知道你分析數來的這點代碼的結果是什麼,咱們這裏來看看結果,而後和上面一步一步的分析一下它的整個的執行過程,就能找到答案:

 

 

      答案就是crash了,其實也是死鎖,下面一步一步的走一下這整個過程,分析一下哪裏死鎖了:

  1. 主線程主隊列中執行任務initDispatchQueue,進入了這個方法,在這個方法裏面建立了一個串行隊列,這一步相信你們都明白,沒什麼問題。
  2. 給這個串行隊列添加了一個異步任務,因爲是異步任務,因此會開啓一條新的線程,爲了方便描述,咱們把新開的這個線程記作線程A, 把這個任務記作任務A,也因爲是異步任務,主線程就不會等待這個任務返回,就接着往下執行其餘任務了。
  3. 接下來的分析就到了這個線程A上,這個任務A被添加到串行隊列以後就開始在線程A上執行,打印出了咱們的第一條信息,也證實了不是在主線程,這個也沒問題。
  4. 線程A開始執行這個任務A,進入這個任務A以後在這個任務A裏面又同步在串行隊列裏面添加任務,記作任務B,因爲任務B是dispatch_sync函數同步添加的,須要立馬被執行,就等待線程A執行它
  5. 可是這個任務B是添加到串行隊列的末尾的,線程A在沒有執行完當前任務A是不會去執行它的,這樣就形成線程A在等待當前任務A執行完,任務B又在等待線程A執行它,就造成了死鎖

      通過上面的分析,你就能看到這個場景和你在主線程同步添加任務是同樣的,咱們再仔細的考慮一下這整個過程,在分析一下上面主線程+串行隊列+同步任務爲何沒有造成死鎖!相互對比理解,就能把整個問題想明白。

 

主角GCD  --  並行隊列


 

      下面咱們接着再說說這個並行隊列,並行隊列+同步執行或者並行隊列+異步執行這個咱們也就沒什麼好說的了,在這裏說說並行+異步的須要注意的地方,不知道你們有沒有想過,並行的話不少任務會一塊兒執行,要是異步任務的話會開啓新的線程,那是否是咱們添加了十個異步任務就會開啓十條線程呢?那一百個異步任務豈不是要開啓一百條線程,答案確定是否認的!那系統究竟是怎麼處理的,咱們也說說,下面的是高級編程書裏面的解釋咱們梳理一下給出結論。

  • 當爲DISPATCH_QUEUE_CONCURRENT的時候,不用等待前面任務的處理結束,後面的任務也是可以直接執行的
  • 並行執行的處理數量取決於當前系統的狀態,即iOS和OS X基於Dispatch Queue中的處理數、CPU核數以及CPU負荷等當前系統狀態來決定DISPATCH_QUEUE_CONCURRENT中並行執行的處理數
  • iOS 和 OS X的核心 -- XNU內核決定應當使用的線程數,而且生成所需的線程執行處理
  • 當處理結束,應當執行的處理數減小時,XNU內核會結束不在須要的線程
  • 處理並行異步任務時候線程是能夠循環往復使用的,好比任務1的線程執行完了任務1,線程能夠接着去執行後面沒有執行的任務

      這裏的東西就這些,咱們在前面串行隊列的時候,串行隊列+異步任務嵌套同步任務會形成死鎖,那咱們要是把它變成同步隊列呢?結果又會是什麼樣子呢?咱們看看下面這段代碼的執行結果:     

 

      從上面的結果能夠看得出來,是沒有問題的,這裏咱們就不在一步一步的分析它的執行過程了,就說說爲何並行的隊列就沒有問題,可是串行的隊列就會出問題:

      並行隊列添加了異步任務也是建立了一個新的線程,而後再在這個任務裏面給並行隊列添加一個同步任務,因爲是並行隊列 ,執行這個同步任務是不須要前面的異步任務執行完了,就直接開始執行,因此也就有了下面的打印信息,經過上面幾個問題,相信理解了以後,對於串行隊列或者並行隊列添加同步任務或者異步任務都有了一個比較深的理解了,咱們再接着往下總結。

 

GCD不只僅這些 


 

    關於GCD的內容還有下面這些都是值得咱們關注的,下面咱們開始一一說一說:

  • dispatch_barrier_async

        dispatch_barrier_async 函數是咱們俗稱的柵欄方法,「柵欄」的意思理解一下字面的,就是把外面和裏面阻隔開,這個函數的做用就是這樣,把插入的這個柵欄以前和以後的阻隔開,等前面的執行完了就執行「柵欄函數」插入的任務,等柵欄的任務執行結束了就開始執行柵欄後面的任務。看下面一個簡單的Demo就理解了。

      從上面就能夠看到,咱們把0插入到第三個任務的位置,它是等前面的兩個任務執行完了,在去執行第三個,要是你以爲這裏前兩個任務簡單,執行不須要太多的時間的話,你能夠試着把前面兩個任務的「任務量」設置大一點,這樣有助於你更好的理解這個「柵欄」操做!

  • dispatch_after

        dispatch_after 延時操做

        若是某一條任務你想等多少時間以後再執行的話,你就徹底可使用這個函數處理,寫法很簡單,由於已經幫咱們封裝好了,看下面這兩行代碼:

        // DISPATCH_TIME_NOW 當前時間開始
        // NSEC_PER_SEC 表示時間的宏,這個能夠本身上網搜索理解
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                
                NSLog(@"延遲了10秒執行");
        });
  • dispatch_apply

       dispatch_apply 相似一個for循環,會在指定的dispatch queue中運行block任務n次,若是隊列是併發隊列,則會併發執行block任務,dispatch_apply是一個同步調用,block任務執行n次後才返回。 因爲它是同步的,要是咱們下面這樣寫就會有出問題:

      能夠看到出問題了,但咱們要是把它放在串行隊列或者並行隊列就會是下面這樣的狀況

     

  • dispatch_group_t

        dispatch_group_t的做用咱們先說說,在追加到Dispatch Queue 中的多個任務所有結束以後想要執行結束的處理,這種狀況也會常常的出現,在只使用一個Serial Dispatch Queue時,只要將想執行的操做所有追加該Serial Dispatch Queue中而且追加在結束處理就能夠實現,可是在使用 Concurrent Dispatch Queue 時或者同時使用多個 Dispatch Queue時候,就比較的複雜了,在這樣的狀況下 Dispatch Group 就能夠發揮它的做用了。看看下面的這段代碼:

-(void)testDispatch_group_t{

        dispatch_group_t group_t = dispatch_group_create();
        dispatch_queue_t queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_group_async(group_t, queue_t, ^{
                
                NSLog(@"1--當前的線程%@",[NSThread currentThread]);
        });
        dispatch_group_async(group_t, queue_t, ^{
                
                NSLog(@"2--當前的線程%@",[NSThread currentThread]);
        });
        dispatch_group_async(group_t, queue_t, ^{
                
                NSLog(@"3--當前的線程%@",[NSThread currentThread]);
        });
        dispatch_group_async(group_t, queue_t, ^{
                
                for (int i = 1; i<10; i++) {
                        
                     NSLog(@"4--當前的線程%@",[NSThread currentThread]);
                }
        });
        // 當前的全部的任務都執行結束
        dispatch_group_notify(group_t, queue_t, ^{
           
                NSLog(@"前面的全都執行結束了%@",[NSThread currentThread]);
        });
}

      這段代碼的意圖很明顯,看了下面的打印信息這個你也就理解它了:

      總結: 關於多線程的最基本的問題暫時先總結這麼多,還有許多的問題,本身也在總結當中,好比如下線程鎖等等的問題,等總結到差很少的時候再分享!

相關文章
相關標籤/搜索