iOS 從實際出發理解多線程
前言
多線程很多開發者多多少少相信也都有了解,以前有些東西理解的不是很透,慢慢的積累之後,這方面的東西也需要自己好好的總結一下。多線程從我剛接觸到iOS的時候就知道這玩意挺重要的,但那時也是能力有限,沒辦法很好的理解它,要是只是查它的概念性的東西,網上一搜一大把,我們再那樣去總結就顯得意義不大了。這篇文章從我剛開始構思著去寫的時候,就希望自己能換個角度去寫,想從實際問題出發總結多線程,那就從第三方以及自己看到的一些例子還有前段時間讀的多線程和內存管理的書中分析理解總結一下多線程。
這幾個概念很容易繞暈
一 進程:進程就是線程的容器,你打開一個App就是打開了一個進程,QQ有QQ的進程,微信有微信的進程,一個進程可以包含多個線程,要是把進程比喻成一條高速公路,線程就是高速路上的一條條車道,也正是因為有了這些車道,整個交通的運行效率變得更高,也正是因為有了多線程的出現,整個系統運行效率變得更高。
二 線程:線程就是在進程中我麽開辟的一條條為我們做事的進程實體,總結的通俗一點,線程就是我們在進程上開辟的一條條做我們想做的事的通道。 一條線程在一個時間點上只能做一件“事”,多線程在同一時間點上,就能做多件“事”,這個理解,還是我們前面說的高速路的例子。
一條高速路是一個進程, 一條條車道就是不同的線程,在過收費站的時候,這條進程上要是只有一條線程,也就是一條高速路上只有一個車道,那你就只能排隊一輛一輛的通過,同一時間不可能有兩輛車一起過去,但要是你一個進程上有多個線程,也就是高速路上有幾個車道,也就有多個窗口收費,這樣的話同一時間就完全有可能兩輛車一起交完費通過了,這樣說相信也能理解這個進程和線程的關系了。
- 同步線程:同步線程會阻塞當前的線程去執行同步線程裏面想做的“事”(任務),執行完之後才會返回當前線程。
- 異步線程:異步線程不會阻塞當前的線程去執行異步線程裏面想做的“事”,因為是異步,所以它會重新開啟一個線程去做想做的“事”。
三 隊列:隊列就是用來管理下面說的“任務”的,它采用的是先進先出(FIFO)的原則,它衍生出來的就是下面的它的分類並行和串行隊列,一條線程上可以有多個隊列。
- 並行隊列:這個隊列裏面的任務是可以並發(同時)執行的,由於我們知道,同步執行任務不會開啟新的線程,所以並行隊列同步執行任務任務只會在一條線程裏面同步執行這些任務,又由於同步執行也就是在當前線程中做事,這個時候就需要一件一件的讓“事”(任務)做完在接著做下一個。但要是是並發隊列異步執行,就對應著開啟異步線程執行要做的“事”(任務),就會同一時間又許多的“事”被做著。
- 串行隊列:這個隊列裏面的任務是串行也就是一件一件做的,串行同步會一件一件的等事做完再接著做下一件,要是異步的就會開啟一條新的線程串行的執行我們的任務。
四 任務:任務按照自己通俗一點的理解,就是提到的“事”這個概念,這個“事”就可以理解為任務,那這個“事”也肯定是在線程上面執行的(不管是在當前線程還是你另開啟的線程)。這個“事”你可以選擇同步或者而是異步執行,這就衍生出了東西也就契合線程上面的同步線程和異步線程。
- 同步任務:不需要開啟新的線程,在當前線程執行就可以。
- 異步任務:你需要開辟一條新的線程去異步的執行這個任務。
iOS當中還有一個特殊的串行隊列-- 主隊列, 這個主隊列中運行著一條特殊的線程 -- 主線程
主線程又叫UI線程,UI線程顧名思義主要的任務及時處理UI,也只有主線程有處理UI的能力,其他的耗時間的操作我們就放在子線程(也就是開辟線程)去執行,開線程也會占據一定的內存的,所以不要同時開啟很多的線程。
通過上面的內容解釋了多線程裏面幾個關鍵的概念的東西,要是有不理解的地方歡迎多交流,下面再給出隊列執行時候的一個運行的表格,我們一個一個慢慢的解釋。
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 -- 主線程
1、我們先從主隊列,主線程開始說起,通過下面的方法我們就可以獲取得到主隊列:
dispatch_queue_t mainqueue = dispatch_get_main_queue();
2、我們在主線程同步執行任務,下面是操作的結果以及打印的信息:
我們解釋一下為什麽在主線程中執行同步任務會出現這個結果,我們一步一步的梳理一下這個執行過程:
- 獲取到在主隊列主線程中執行了最前面的打印信息,這個沒什麽問題
- 開始執行dispatch_sync這個函數,主隊列是串行隊列,這個函數會把這個任務插入到主隊列的最後面(理解隊列添加任務)
- 主線程執行到這裏的時候就會等待插入的這個同步任務執行完之後再執行後面的操作
- 但由於這個同步任務是插入到主隊列的最後面,最隊列前面的任務沒有執行完之前是不會執行這個block的(主線程在執行initMainQueue任務)
- 這樣就造成了一個相互等待的過程,主線程在等待block完返回,block卻在等待主線程執行它,這樣就造成了死鎖,看打印的信息你也就知道block是沒有被執行的。
這裏我們你可能會思考,主隊列是一個串行隊列,那我們在主線程中添加一個串行隊列,再給串行隊列添加一個同步任務,這時候和前面主線程主隊列添加同步任務不就場景一樣了嗎?那結果呢? 我們看看下面的打印:
我們按照前面的方式解釋一下這個的執行步驟:
- 主線程在執行主隊列中的方法initSerialQueue,到這個方法時候創建了一個串行隊列(註意不是主隊列)打印了前面的第一條信息
- 執行到dispatch_sync函數,這個函數給這個串行隊列中添加了一個同步任務,同步任務是會立馬執行的
- 主線程就直接操作執行了這個隊列中的同步任務,打印的第二條信息
- 主線程接著執行下面的第三條打印信息
理解:看這個執行的過程對比前面的,你就知道了不同的地方就是前面是添加在了主隊列當中,但這裏有添加到主隊列,由於是插入到主隊列的末尾,所以需要主隊列的任務都執行完才能指定到它,但主線程執行到initMainQueue這個方法的時候在等待這個方法中添加的同步任務執行完接著往下執行,但它裏面的同步任務又在等待主線程執行完在執行它,就相互等待了,但主線程執行不是主隊列裏面的同步任務的時候是不需要主線程執行完所有操作在執行這個任務的,這個任務是它添加到串行隊列的開始也是結束的任務,由於不需要等待,就不會造成死鎖!
上面這個問題經常會看到有人問,有許多解釋,也希望自己能把這個問題給說清楚了!
3、主線程這裏我們再提一點,就是線程間的信息簡單傳遞
前面我們有說到主線程又叫做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了,其實也是死鎖,下面一步一步的走一下這整個過程,分析一下哪裏死鎖了:
- 主線程主隊列中執行任務initDispatchQueue,進入了這個方法,在這個方法裏面創建了一個串行隊列,這一步相信大家都明白,沒什麽問題。
- 給這個串行隊列添加了一個異步任務,由於是異步任務,所以會開啟一條新的線程,為了方便描述,我們把新開的這個線程記做線程A, 把這個任務記做任務A,也由於是異步任務,主線程就不會等待這個任務返回,就接著往下執行其他任務了。
- 接下來的分析就到了這個線程A上,這個任務A被添加到串行隊列之後就開始在線程A上執行,打印出了我們的第一條信息,也證明了不是在主線程,這個也沒問題。
- 線程A開始執行這個任務A,進入這個任務A之後在這個任務A裏面又同步在串行隊列裏面添加任務,記做任務B,由於任務B是dispatch_sync函數同步添加的,需要立馬被執行,就等待線程A執行它
- 但是這個任務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]); }); }
這段代碼的意圖很明顯,看了下面的打印信息這個你也就理解它了:
總結: 關於多線程的最基本的問題暫時先總結這麽多,還有許多的問題,自己也在總結當中,比如以下線程鎖等等的問題,等總結到差不多的時候再分享!
iOS 從實際出發理解多線程