iOS线程知识整理
什么是線程
1、線程的定義、狀態、屬性
進程
進程:(Process)是計算機中的程序關于某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷,一個進程包含1--n個線程。(進程是資源分配的最小單位)線程
線程:有時被稱為輕量級進程(Lightweight Process,LWP),是程序執行流的最小單元。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。另外,線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不擁有系統資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創建和撤消另一個線程,同一進程中的多個線程之間可以并發執行。由于線程之間的相互制約,致使線程在運行中呈現出間斷性。線程也有就緒、阻塞和運行三種基本狀態。就緒狀態是指線程具備運行的所有條件,邏輯上可以運行,在等待處理機; 運行狀態是指線程占有處理機正在運行; 阻塞狀態是指線程在等待一個事件(如某個信號量),邏輯上不可執行。每一個程序都至少有一個線程,若程序只有一個線程,那就是程序本身。多線程:線程是程序中一個單一的順序控制流程。進程內一個相對獨立的、可調度的執行單元,是系統獨立調度和分派CPU的基本單位指運行中的程序的調度單位。在單個程序中同時運行多個線程完成不同的工作,稱為多線程。同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換開銷小。(線程是cpu調度的最小單位)多進程是指操作系統能同時運行多個任務(程序)。多線程是指在同一程序中有多個順序流在執行。線程與進程的共同點和區別
共同點:線程和進程一樣分為五個階段:創建、就緒、運行、阻塞、終止。區別:線程和進程的區別在于,子進程和父進程有不同的代碼和數據空間,而多個線程則共享數據空間,每個線程有自己的執行堆棧和程序計數器為其執行上下文。多線程主要是為了節約CPU時間,發揮利用,根據具體情況而定。線程的運行中需要使用計算機的內存資源和CPU。線程與進程的區別可以歸納為以下幾點:1)地址空間和其它資源(如打開文件):進程間相互獨立,同一進程的各線程間共享。某進程內的線程在其它進程不可見。2)通信:進程間通信IPC,線程間可以直接讀寫進程數據段(如全局變量)來進行通信——需要進程同步和互斥手段的輔助,以保證數據的一致性。3)調度和切換:線程上下文切換比進程上下文切換要快得多。4)在多線程OS中,進程不是一個可執行的實體。線程的狀態
就緒:線程分配了CPU以外的全部資源,等待獲得CPU調度執行:線程獲得CPU,正在執行阻塞:線程由于發生I/O或者其他的操作導致無法繼續執行,就放棄處理機,轉入線程就緒線程的特性
線程在多線程OS中,通常是在一個進程中包括多個線程,每個線程都是作為利用CPU的基本單位,是花費最小開銷的實體。線程具有以下屬性。①輕型實體線程中的實體基本上不擁有系統資源,只是有一點必不可少的、能保證獨立運行的資源,比如,在每個線程中都應具有一個用于控制線程運行的線程控制塊TCB,用于指示被執行指令序列的程序計數器、保留局部變量、少數狀態參數和返回地址等的一組寄存器和堆棧。②獨立調度和分派的基本單位。在多線程OS中,線程是能獨立運行的基本單位,因而也是獨立調度和分派的基本單位。由于線程很“輕”,故線程的切換非常迅速且開銷小。③可并發執行。在一個進程中的多個線程之間,可以并發執行,甚至允許在一個進程中所有線程都能并發執行;同樣,不同進程中的線程也能并發執行。④共享進程資源。在同一進程中的各個線程,都可以共享該進程所擁有的資源,這首先表現在:所有線程都具有相同的地址空間(進程的地址空間),這意味著,線程可以訪問該地址空間的每一個虛地址;此外,還可以訪問進程所擁有的已打開文件、定時器、信號量機構等。2、線程之間的通信
什么是線程通信
多個線程在處理同一個資源,并且任務不同時,需要線程通信來幫助解決線程之間對同一個變量的使用或操作。就是多個線程在操作同一份數據時, 避免對同一共享變量的爭奪。就是在一個線程進行了規定操作后,就進入等待狀態(wait), 等待其他線程執行完他們的指定代碼過后 再將其喚醒(notify);當我們創建多個生產者和消費者時,無法直到到底要喚醒哪一個,所以這時候我們就用到了notifAll()方法。為什么要線程通信
多個線程并發執行時, 在默認情況下CPU是隨機切換線程的,當我們需要多個線程來共同完成一件任務,并且我們希望他們有規律的執行, 那么多線程之間需要一些協調通信,以此來幫我們達到多線程共同操作一份數據。當然如果我們沒有使用線程通信來使用多線程共同操作同一份數據的話,雖然可以實現,但是在很大程度會造成多線程之間對同一共享變量的爭奪,那樣的話勢必為造成很多錯誤和損失!所以,我們才引出了線程之間的通信,多線程之間的通信能夠避免對同一共享變量的爭奪。3、線程進程以及堆棧關系的總結
棧是線程獨有的,保存其運行狀態和局部自動變量的,棧在線程開始的時候初始化,每個線程的棧相互對立,因此,棧是線程安全的,??臻g有系統管理。棧被自動分配到進程的內存空間中。
堆在操作系統度進程初始化的時候分配,運行過程中也可以向系統要額外的堆,但是用完要返還,不然就是內存泄露。
iOS中的線程
iOS中提供了四套多線程方案、一種一種來看。
Pthreads (不做介紹) NSThread GCD NSOperation & NSOperationQueue1、NSThread
蘋果封裝、面向對象的、可以直接操控線程對象,非常直觀和方便。但是,它的生命周期還是需要我們手動管理。優缺點
優點:輕量級缺點:一個NSThread對象代表一個線程,需要手動管理線程的生命周期,處理線程同步等問題,線程同步對數據的加鎖會有一定的開銷。創建并啟動
1、先創建線程類,再啟動
//1 創建NSThread 并啟動 NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];[thread start];2、創建并自動啟動
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];// __weak typeof(self) weakself = self; [NSThread detachNewThreadWithBlock:^{[weakself run]; }];3、使用 NSObject 的方法創建并自動啟動
[self performSelectorInBackground:@selector(run) withObject:nil];但是在Swift中沒有這個方法:
Note: The performSelector: method and related selector-invoking methods are not imported in Swift because they are inherently unsafe. //共同執行的方法\兩種鎖 - (void)run { // [lock lock]; // NSLog(@"111111"); // NSLog(@"---%@",NSThread.currentThread); // [lock unlock];@synchronized (self) {NSLog(@"111111%@",NSThread.currentThread);NSLog(@"---%@",NSThread.currentThread);} }執行結果: 2017-11-09 10:55:34.196040+0800 MultithreadingDemo[96041:6103178] 111111<NSThread: 0x604000273200>{number = 5, name = (null)} 2017-11-09 10:55:34.196457+0800 MultithreadingDemo[96041:6103178] ---<NSThread: 0x604000273200>{number = 5, name = (null)} 2017-11-09 10:55:34.197956+0800 MultithreadingDemo[96041:6103177] 111111<NSThread: 0x604000273100>{number = 3, name = (null)} 2017-11-09 10:55:34.198510+0800 MultithreadingDemo[96041:6103177] ---<NSThread: 0x604000273100>{number = 3, name = (null)} 2017-11-09 10:55:34.200726+0800 MultithreadingDemo[96041:6103179] 111111<NSThread: 0x604000273140>{number = 4, name = (null)} 2017-11-09 10:55:34.201170+0800 MultithreadingDemo[96041:6103179] ---<NSThread: 0x604000273140>{number = 4, name = (null)}其他方法
除了創建啟動外,NSThread 還以很多方法,下面是一些常見的方法
//取消線程 - (void)cancel;//啟動線程 - (void)start;//判斷某個線程的狀態的屬性 @property (readonly, getter=isExecuting) BOOL executing; @property (readonly, getter=isFinished) BOOL finished; @property (readonly, getter=isCancelled) BOOL cancelled;//獲取當前線程信息 + (NSThread *)currentThread;//獲取主線程信息 + (NSThread *)mainThread;//使當前線程暫停一段時間,或者暫停到某個時刻 + (void)sleepForTimeInterval:(NSTimeInterval)time; + (void)sleepUntilDate:(NSDate *)date;2、GCD
Grand Central Dispatch,是libdispatch的市場名稱,而libdispatch是Apple的一個庫,其為并發代碼在iOS和OS X的多核硬件上執行提供支持。確切地說GCD是一套低層級的C API,通過 GCD,開發者只需要向隊列中添加一段代碼塊(block或C函數指針),而不需要直接和線程打交道。GCD在后端管理著一個線程池,它不僅決定著你的代碼塊將在哪個線程被執行,還根據可用的系統資源對這些線程進行管理。這樣通過GCD來管理線程,從而解決線程生命周期(創建線程、調度任務、銷毀線程)問題。同時自動合理地利用更多的CPU內核(比如雙核、四核)。
GCD 優點
易用: GCD 提供一個易于使用的并發模型而不僅僅只是鎖和線程,以幫助我們避開并發陷阱,而且因為基于block,它能極為簡單得在不同代碼作用域之間傳遞上下文。
靈活: GCD 具有在常見模式上(比如鎖、單例),用更高性能的方法優化代碼,而且GCD能提供更多的控制權力以及大量的底層函數。
性能: GCD 能自動根據系統負載來增減線程數量,這就減少了上下文切換以及增加了計算效率。
GCD 概念
1.Dispatch Object
GCD被組建成面向對象的風格。GCD對象被稱為 dispatch object, 所有的 dispatch object 都是OC對象.,就如其他OC對象一樣,當開啟了 ARC 時,dispatch object 的retain和release都會自動執行。而如果是MRC的話,dispatch objects會使用dispatch_retain和dispatch_release這兩個方法來控制引用計數。
在 iOS 6.0 dispatch_release 已被廢棄。內部被改成對象釋放(release)所以 arc 后都不再使用
2.Serial & Concurrent
串行任務就是每次只有一個任務被執行,并發任務就是在同一時間可以有多個任務被執行。
3.Synchronous & Asynchronous
Synchronous(同步函數)意思是在完成了它預定的任務后才返回,在任務執行時會阻塞當前線程。而 Asynchronous(異步函數)則是任務會完成但不會等它完成,所以異步函數不會阻塞當前線程,會繼續去執行下去。
4.Concurrency & Parallelism
Concurrency (并發)的意思就是同時運行多個任務。這些任務可能是以在單核 CPU 上以分時(時間共享)的形式同時運行,也可能是在多核 CPU 上以真正的并行方式來運行。然后為了使單核設備也能實現這一點,并發任務必須先運行一個線程,執行一個上下文切換,然后運行另一個線程或進程。Parallelism(并行)則是真正意思上的多任務同時運行。
5.Context Switch
Context Switch即上下文切換,一個上下文切換指當你在單個進程里切換執行不同的線程時存儲與恢復執行狀態的過程。這個過程在編寫多任務應用時很普遍,但會帶來一些額外的開銷。
6.Dispatch Queues
GCD dispatch queues 是一個強大的執行多任務的工具。Dispatch queue 是一個對象,它可以接受任務,并將任務以先進先出(FIFO)的順序來執行。Dispatch queue 可以并發的或串行的執行任意一個代碼塊,而且并發任務會像 NSOperationQueue 那樣基于系統負載來合適地并發進行,串行隊列同一時間則只執行單一任務。Dispatch queues 內部使用的是線程,GCD 管理這些線程,并且使用 Dispatch queues 的時候,我們都不需要自己創建線程。Dispatch queues相對于和線程直接通信的代碼優勢是:使用起來特別方便,執行任務更加有效率。
7.Queue Types
- main queue : 主隊列 (主線程)
- global queue : 全局隊列 (有多個線程)
- custom queue : 自定義隊列 (串行:單線程 ,并行:有多個線程)
GCD的具體使用
1.添加任務到隊列
GCD有兩種方式來把任務添加到隊列中:異步和同步。
異步方式添加任務到隊列的情況:
1.自定義串行隊列:按添加進隊列的先后順序 順序執行(不管同步異步線程)
我們接著上面的run方法來寫一個串行隊列第一步,寫兩個異步線程和一個同步線程加入隊列執行:其中第一個線程執行任務之前睡眠1秒 [self run]; __weak typeof(self) weakself = self; dispatch_queue_t queue = dispatch_queue_create("串行隊列", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{sleep(1);[weakself run]; }); dispatch_async(queue, ^{[weakself run]; });dispatch_sync(queue, ^{[weakself run]; });結果: 2017-11-09 14:00:22.642407+0800 MultithreadingDemo[97236:6195716] ---<NSThread: 0x604000069240>{number = 1, name = main} 2017-11-09 14:00:23.643340+0800 MultithreadingDemo[97236:6195829] ---<NSThread: 0x604000275300>{number = 3, name = (null)} 2017-11-09 14:00:23.643547+0800 MultithreadingDemo[97236:6195829] ---<NSThread: 0x604000275300>{number = 3, name = (null)} 2017-11-09 14:00:23.643776+0800 MultithreadingDemo[97236:6195716] ---<NSThread: 0x604000069240>{number = 1, name = main}包括主線程在內,整個隊列里面有兩條線程,但是執行結果卻被第一個sleep阻塞1秒。所以串行隊列是一個個任務完成后再執行后面的任務第二步,寫一個異步線程包裹一個同步線程,并在同步線程中執行run [self run]; __weak typeof(self) weakself = self; dispatch_queue_t queue = dispatch_queue_create("串行隊列", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{[weakself run]; }); dispatch_async(queue, ^{ //1號任務[weakself run];dispatch_sync(queue, ^{ //2號任務[weakself run];}); }); 結果: 2017-11-09 14:08:03.335534+0800 MultithreadingDemo[97291:6200400] ---<NSThread: 0x60000006f580>{number = 1, name = main} 2017-11-09 14:08:03.335846+0800 MultithreadingDemo[97291:6200500] ---<NSThread: 0x60000046a9c0>{number = 3, name = (null)} 2017-11-09 14:08:03.336308+0800 MultithreadingDemo[97291:6200500] ---<NSThread: 0x60000046a9c0>{number = 3, name = (null)}崩潰在 dispatch_sync 這一行,所以我們只看到了一條主線程run的記錄 分析一下: 1、我們使用了同步線程,而且是串行隊列, 2、1號任務沒有結束、2號任務是無法執行的 3、當任務走到同步線程開啟的時候,線程會被阻塞,直到2號任務block內的任務執行完成才會釋放 4、可是同步線程把任務加入queue隊列之后才發現,自己要執行的這個任務前面還卡著一個1號任務 5、線程被阻塞,1號任務無法完成,1號任務沒完成 2號任務就不能執行 6、造成死鎖所以改一下,只要把同步任務換個隊列執行,就可以避免死鎖了: dispatch_async(queue, ^{[weakself run];dispatch_sync(dispatch_get_main_queue(), ^{[weakself run];}); });2.主隊列:順序執行、串行隊列 一般更新UI都在主線程。
//主隊列中的任務一定會回到主線程去執行、如下方式去執行,同步任務在主線程、主隊列執行,主隊列是串行隊列,又會出現死鎖 dispatch_sync(dispatch_get_main_queue(), ^{[weakself run]; }); 改成: dispatch_async(dispatch_get_main_queue(), ^{[weakself run]; });3.并發隊列:非順序執行,隨機、同步執行并發隊列一樣會卡住主線程
如串行隊列所寫,在并行隊列寫相同代碼執行結果會如何: [self run]; __weak typeof(self) weakself = self; dispatch_queue_t queue = dispatch_queue_create("并行隊列", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{[weakself run]; }); dispatch_async(queue, ^{[weakself run];dispatch_sync(queue, ^{[weakself run];}); });結果: 2017-11-09 14:34:15.075513+0800 MultithreadingDemo[97572:6219452] ---<NSThread: 0x60000007dbc0>{number = 1, name = main} 2017-11-09 14:34:15.075965+0800 MultithreadingDemo[97572:6219573] ---<NSThread: 0x60000046bdc0>{number = 4, name = (null)} 2017-11-09 14:34:15.075967+0800 MultithreadingDemo[97572:6219574] ---<NSThread: 0x60000046ae00>{number = 3, name = (null)} 2017-11-09 14:34:15.076703+0800 MultithreadingDemo[97572:6219573] ---<NSThread: 0x60000046bdc0>{number = 4, name = (null)}可以看到執行結果是正常的,并未出現死鎖,那是因為并行隊列是可以多個任務并行執行的,正因為允許多個任務同時執行,所以執行結束時間并不是按著添加入隊列的順序來的。4.全球隊列:并行隊列、異步線程常用隊列
dispatch_get_global_queue(0, 0);
2.并發執行迭代循環
在開發中,并發隊列能很好地提高效率,特別是當我們需要執行一個數據龐大的循環操作時。打個比方來說吧,我們需要執行一個for循環,每一次循環操作如下:
for (i = 0; i < count; i++) {NSLog("%d",i); }GCD提供了一個簡化方法叫做dispatch_apply,當我們把這個方法放到并發隊列中執行時,這個函數會調用單一block多次,并平行運算,然后等待所有運算結束。
代碼示例:
但是dispatch_apply函數是沒有異步版本的。只能將整個dispatch_apply 置于異步中。dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);dispatch_apply(count, queue, ^(size_t i) {NSLog("%d",i); });直接在主線程調用dispatch_apply 會阻塞主線程,如果使用了并發隊列 隊列任務會被放置在異步線程中執行,但是主線程依然被阻塞。只有整個放入異步線程才不會阻塞主線程。3.掛起和恢復隊列
有時候,我們不想讓隊列中的某些任務馬上執行,這時我們可以通過掛起操作來阻止一個隊列中將要執行的任務。當需要掛起隊列時,使用dispatch_suspend方法;恢復隊列時,使用dispatch_resume方法。調用dispatch_suspend會增加隊列掛起的引用計數,而調用dispatch_resume則會減少引用計數,當引用計數大于0時,隊列會保持掛起狀態。因此,這隊列的掛起和恢復中,我們需要小心使用以避免引用計數計算錯誤的出現。
執行掛起操作不會對已經開始執行的任務起作用,它僅僅只會阻止將要進行但是還未開始的任務。 dispatch_queue_t myQueue;myQueue = dispatch_queue_create("隊列", NULL); //掛起隊列 dispatch_suspend(myQueue); //恢復隊列 dispatch_resume(myQueue);如下:__weak typeof(self) weakself = self; dispatch_queue_t queue = dispatch_queue_create("并行隊列", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{[weakself run]; }); dispatch_async(queue, ^{dispatch_suspend(queue);[weakself run];dispatch_sync(queue, ^{[weakself run];}); });結果 只有兩條run語句,同步線程因為隊列被掛起,所以并未執行 2017-11-09 14:43:22.593056+0800 MultithreadingDemo[97644:6225319] ---<NSThread: 0x60000027e0c0>{number = 9, name = (null)} 2017-11-09 14:43:22.592831+0800 MultithreadingDemo[97644:6226170] ---<NSThread: 0x600000271a40>{number = 8, name = (null)}4.dispatch_after 的使用
延遲一段時間把一項任務提交到隊列中執行,返回之后就不能取消
dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);一般我們在做一些延時任務的時候使用的多5.dispatch_once 的使用
保證在APP運行期間,block中的代碼只執行一次
static Demo *demo; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{demo = [Demo new]; });單例常用6.Dispatch Groups 的使用
Dispatch groups是阻塞線程直到一個或多個任務完成的一種方式。在那些需要等待任務完成才能執行某個處理的時候,你可以使用這個方法。Group會在整個組的任務都完成時通知你,這些任務可以是同步的,也可以是異步的,即便在不同的隊列也行。而且在整個組的任務都完成時, Group可以用同步的或者異步的方式通知你。當group中所有的任務都完成時,GCD 提供了兩種通知方式。
dispatch_group_wait。它會阻塞當前線程,直到隊列里面所有的任務都完成或者等到某個超時發生。
代碼示例:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); // 添加隊列到組中 dispatch_group_async(group, queue, ^{ // 一些異步操作 或者耗時操作 });//如果在所有任務完成前超時了,該函數會返回一個非零值。 //你可以對此返回值做條件判斷以確定是否超出等待周期; dispatch_group_wait(group, DISPATCH_TIME_FOREVER);NSLog(@"123"); //被阻塞,因為dispatch_group_wait 所以這一句代碼只會在隊列任務都完成后執行dispatch_group_notify。它以異步的方式工作,當 Dispatch Group中沒有任何任務時,它就會執行其代碼,那么 completionBlock便會運行??梢杂糜谠诓⑿嘘犃兄写腥蝿斩纪瓿芍笤僬{起執行。
代碼示例:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create();// 添加隊列到組中 dispatch_group_async(group, queue, ^{NSLog(@"one---%@",NSThread.currentThread); }); dispatch_group_async(group, queue, ^{// 一些延時操作sleep(2);NSLog(@"two---%@",NSThread.currentThread); }); dispatch_group_async(group, queue, ^{// 一些延時操作sleep(3);NSLog(@"three---%@",NSThread.currentThread); }); dispatch_group_async(group, queue, ^{NSLog(@"four---%@",NSThread.currentThread); });dispatch_group_notify(group, queue, ^{NSLog(@"我會一直等到現在"); }); NSLog(@"123");結果 2017-11-09 15:21:48.480021+0800 MultithreadingDemo[98195:6255855] 123 2017-11-09 15:21:48.480192+0800 MultithreadingDemo[98195:6255916] one---<NSThread: 0x600000466800>{number = 3, name = (null)} 2017-11-09 15:21:48.480321+0800 MultithreadingDemo[98195:6255917] four---<NSThread: 0x600000466840>{number = 4, name = (null)} 2017-11-09 15:21:50.483266+0800 MultithreadingDemo[98195:6255918] two---<NSThread: 0x604000462e80>{number = 5, name = (null)} 2017-11-09 15:21:51.483851+0800 MultithreadingDemo[98195:6255922] three---<NSThread: 0x60400027dd40>{number = 6, name = (null)} 2017-11-09 15:21:51.484084+0800 MultithreadingDemo[98195:6255922] 我會一直等到現在對這一段代碼,并行隊列執行,最后一行不會阻塞,其余加入group中的任務執行完成后才會執行notify中的任務。 常用于需要等待某些異步線程執行完成后統一處理的場景,比如多個接口數據拼裝模型7.dispatch_barrier_async 、dispatch_barrier_sync 的使用
在并行隊列中,為了保持某些任務的順序,需要等待一些任務完成后才能繼續進行,使用 barrier 柵欄函數 來等待之前任務完成,避免數據競爭等問題。
同步,會攔截后面所有的代碼執行,直到前面任務完成,并且完成柵欄函數中的任務。
異步,攔截并行隊列中的后續任務,直到前面任務執行完,并且完成柵欄函數中的任務。不會影響主線程。
dispatch_barrier_async 函數會等待追加到并行隊列中的操作全部執行完之后,然后再執行 dispatch_barrier_async 函數追加的處理,等 dispatch_barrier_async 追加的處理執行結束之后(同時只執行一個任務),Concurrent Dispatch Queue才恢復之前的動作繼續執行。
注意:使用 dispatch_barrier_async,該函數只能搭配自定義并行隊列 dispatch_queue_t 使用。不能使用: dispatch_get_global_queue ,否則 dispatch_barrier_async 的作用會和 dispatch_async 的作用一模一樣。
__weak typeof(self) weakself = self; dispatch_queue_t queue = dispatch_queue_create("并行隊列", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{[weakself run]; }); dispatch_async(queue, ^{[weakself run]; });//加入 dispatch_barrier_async(queue, ^{sleep(1);[weakself run2];sleep(1); }); dispatch_barrier_async(queue, ^{[weakself run2];sleep(1); }); dispatch_async(queue, ^{[weakself run]; }); dispatch_async(queue, ^{[weakself run]; });結果 2017-11-09 16:50:54.226018+0800 MultithreadingDemo[99305:6326134] ---<NSThread: 0x6000002617c0>{number = 4, name = (null)} 2017-11-09 16:50:54.225967+0800 MultithreadingDemo[99305:6326323] ---<NSThread: 0x600000268700>{number = 3, name = (null)} 2017-11-09 16:50:55.227973+0800 MultithreadingDemo[99305:6326323] ++++<NSThread: 0x600000268700>{number = 3, name = (null)} 2017-11-09 16:50:56.228820+0800 MultithreadingDemo[99305:6326323] ++++<NSThread: 0x600000268700>{number = 3, name = (null)} 2017-11-09 16:50:57.230081+0800 MultithreadingDemo[99305:6326323] ---<NSThread: 0x600000268700>{number = 3, name = (null)} 2017-11-09 16:50:57.230082+0800 MultithreadingDemo[99305:6326134] ---<NSThread: 0x6000002617c0>{number = 4, name = (null)}55\56秒 明顯的三次停頓。說明執行 dispatch_barrier_async 插入的任務時 同時只執行了一個任務3、NSOperation
NSOperation 是蘋果公司對 GCD 的封裝,完全面向對象,所以使用起來更好理解。 大家可以看到 NSOperation和 NSOperationQueue 分別對應 GCD 的 任務 和 隊列 。
優缺點
與NSThread的區別:沒有那么輕量級,但是不需要關心線程管理,數據同步的事情。與GCD區別:NSOperationQueue可以方便的管理并發、NSOperation之間的優先級。GCD主要與block結合使用。代碼簡潔高效。如果異步操作的過程需要更多的被交互和UI呈現出來,NSOperationQueue會是一個更好的選擇。底層代碼中,任務之間不太互相依賴,而需要更高的并發能力,GCD則更有優勢我們要做的就是:
1.將要執行的任務封裝到一個NSOperation對象中
2.將此任務添加到一個NSOperationQueue對象中
創建添加
NSOperation有兩個子類:NSBlockOperation 和 NSInvocationOperation (或者自行自定義Operation )
NSBlockOperation:(OC 代碼、Swift也有)
+ (instancetype)blockOperationWithBlock:(void (^)(void))block; - (void)addExecutionBlock:(void (^)(void))block;__weak typeof(self) weakself = self; NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{[weakself run]; }]; [operation start];結果 2017-11-09 15:53:29.765058+0800 MultithreadingDemo[98518:6280532] ---<NSThread: 0x604000073e80>{number = 1, name = main}1、直接執行創建的operation 默認是當前線程 2、NSBlockOperation 還有一個添加執行block的方法,它會在當前線程和其他多個線程執行這些block中的任務 [operation addExecutionBlock:^{[weakself run]; }];結果 2017-11-09 15:53:29.765058+0800 MultithreadingDemo[98518:6280532] ---<NSThread: 0x604000073e80>{number = 1, name = main} 2017-11-09 15:53:29.765055+0800 MultithreadingDemo[98518:6280642] ---<NSThread: 0x60400026ea40>{number = 3, name = (null)}注意:當NSOperation開始執行后不能再添加任務NSInvocationOperation: (Swift 不允許使用)
- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg; - (instancetype)initWithInvocation:(NSInvocation *)inv NS_DESIGNATED_INITIALIZER;//1.創建NSInvocationOperation對象 NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];//2.開始執行 [operation start];隊列
上面例子中的任務執行,不管是多線程還是單線程都必然會在當前線程執行一個任務
NSOperation的隊列和GCD不同,不存在串行、并行之分,他們只有主隊列和其他隊列:
主隊列:
NSOperationQueue *queue = [NSOperationQueue mainQueue];其他隊列:(注意:其他隊列的任務會在其他線程并行執行)
所有的非主隊列就是其他隊列,也就是說不是通過 mainQueue 獲取的隊列都是其他隊列NSOperationQueue *queue = [[NSOperationQueue alloc]init]; __weak typeof(self) weakself = self; NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{[weakself run]; }]; [operation addExecutionBlock:^{[weakself run]; }]; // [operation start]; 只要加入隊列,任務就會自動start [queue addOperation:operation];或者[queue addOperationWithBlock:^{[weakself run]; }];其實更多來看 NSOperation相當于一個任務組,里面可以裝多個任務,然后任務組被加入隊列去執行
那么問題來了:沒有串行隊列么?按前面說的,所有任務會在其他線程同步執行,那我希望一個個執行怎么辦?
NSOperationQueue 有一個參數:maxConcurrentOperationCount這個參數表示允許并發執行的任務數限制,當為1的時候其實也就是串行執行了NSOperationQueue *queue = [[NSOperationQueue alloc]init]; __weak typeof(self) weakself = self; NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{[weakself run]; }]; [operation addExecutionBlock:^{sleep(1);[weakself run]; }]; queue.maxConcurrentOperationCount = 1; [queue addOperation:operation]; [queue addOperationWithBlock:^{[weakself run]; }];結果2017-11-09 16:18:23.524428+0800 MultithreadingDemo[98831:6301089] ---<NSThread: 0x600000473840>{number = 3, name = (null)} 2017-11-09 16:18:24.524800+0800 MultithreadingDemo[98831:6301087] ---<NSThread: 0x60000046d640>{number = 4, name = (null)} 2017-11-09 16:18:24.525121+0800 MultithreadingDemo[98831:6301087] ---<NSThread: 0x60000046d640>{number = 4, name = (null)}其他功能
依賴:NSOperation還有一個非常實用的功能,也就是添加依賴
NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{sleep(1);NSLog(@"拉取A接口--%@",NSThread.currentThread); }];NSBlockOperation *operationB = [NSBlockOperation blockOperationWithBlock:^{sleep(1);NSLog(@"通過A接口參數拉取B接口--%@",NSThread.currentThread); }];NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{sleep(1);NSLog(@"通過B接口參數拉取C接口--%@",NSThread.currentThread); }]; [operationB addDependency:operationA]; [operationC addDependency:operationB];NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [queue addOperations:@[operationA, operationB, operationC] waitUntilFinished:NO];隊列允許多個任務同時執行,但因為三個任務之間的依賴,我們看一下結果:2017-11-09 16:25:56.598192+0800 MultithreadingDemo[98972:6307395] 拉取A接口--<NSThread: 0x6040002748c0>{number = 3, name = (null)} 2017-11-09 16:25:57.599920+0800 MultithreadingDemo[98972:6307396] 通過A接口參數拉取B接口--<NSThread: 0x60000046d680>{number = 4, name = (null)} 2017-11-09 16:25:58.600665+0800 MultithreadingDemo[98972:6307395] 通過B接口參數拉取C接口--<NSThread: 0x6040002748c0>{number = 3, name = (null)}注意: 使用依賴的時候,我們要注意一點,依賴不能產生循環依賴,不然會死鎖 可以使用 removeDependency 來解除依賴關系。 不同的隊列之間的任務也可以依賴4、鎖
NSLock
NSLock 遵循 NSLocking 協議,lock 方法是加鎖unlock 是解鎖tryLock 是嘗試加鎖,如果失敗的話返回 NOlockBeforeDate: 是在指定Date之前嘗試加鎖,如果在指定時間之前都不能加鎖,則返回NO。NSConditionLock 條件鎖
@property (readonly) NSInteger condition; - (void)lockWhenCondition:(NSInteger)condition; - (BOOL)tryLock; - (BOOL)tryLockWhenCondition:(NSInteger)condition; - (void)unlockWithCondition:(NSInteger)condition; - (BOOL)lockBeforeDate:(NSDate *)limit; - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;NSConditionLock 和 NSLock 類似,都遵循 NSLocking 協議,方法都類似,只是多了一個 condition 屬性,以及每個操作都多了一個關于 condition 屬性的方法NSConditionLock 可以稱為條件鎖:tryLockWhenCondition:(NSInteger)condition; 只有 condition 參數與初始化時候的 condition 相等,lock 才能正確進行加鎖操作。unlockWithCondition:(NSInteger)condition; 解鎖后 condition 的值更新為新的值NSRecursiveLock 遞歸鎖
NSRecursiveLock 是遞歸鎖,他和 NSLock 的區別在于,NSRecursiveLock 可以在一個線程中重復加鎖(反正單線程內任務是按順序執行的,不會出現資源競爭問題),NSRecursiveLock 會記錄上鎖和解鎖的次數,當二者平衡的時候,才會釋放鎖,其它線程才可以上鎖成功。如下遞歸操作,block中每次有加鎖操作,再未解鎖的時候再次進入遞歸,再次加鎖,造成死鎖。NSRecursiveLock就是用來解決這個問題的。 NSLock *normal_lock = [NSLock new]; NSRecursiveLock *recu_lock = [NSRecursiveLock new]; //線程1 dispatch_async(dispatch_get_main_queue(), ^{static void (^Block)(int);Block = ^(int value) {[normal_lock lock];if (value > 0) {NSLog(@"value:%d", value);Block(value - 1);}[normal_lock unlock];};Block(5); });NSCondition
- (void)wait; - (BOOL)waitUntilDate:(NSDate *)limit; - (void)signal; - (void)broadcast;NSCondition 的對象實際上作為一個鎖和一個線程檢查器,鎖上之后其它線程也能上鎖,而之后可以根據條件決定是否繼續運行線程,即線程是否要進入 waiting 狀態,經測試,NSCondition 并不會像上文的那些鎖一樣,先輪詢,而是直接進入 waiting 狀態,當其它線程中的該鎖執行 signal 或者 broadcast 方法時,線程被喚醒,繼續運行之后的方法。用法如下:NSCondition *lock = [[NSCondition alloc] init];NSMutableArray *array = [[NSMutableArray alloc] init];//線程1dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{[lock lock];while (!array.count) {[lock wait];}[array removeAllObjects];NSLog(@"array removeAllObjects");[lock unlock];});//線程2dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{sleep(1);//以保證讓線程2的代碼后執行[lock lock];[array addObject:@1];NSLog(@"array addObject:@1");[lock signal];[lock unlock];}); 也就是使用 NSCondition 的模型為:鎖定條件對象。測試是否可以安全的履行接下來的任務。如果布爾值是假的,調用條件對象的 wait 或 waitUntilDate: 方法來阻塞線程。 在從這些方法返回,則轉到步驟 2 重新測試你的布爾值。 (繼續等待信號和重新測試,直到可以安全的履行接下來的任務。waitUntilDate: 方法有個等待時間限制,指定的時間到了,則放回 NO,繼續運行接下來的任務)如果布爾值為真,執行接下來的任務。當任務完成后,解鎖條件對象。而步驟 3 說的等待的信號,既線程 2 執行 [lock signal] 發送的信號。其中 signal 和 broadcast 方法的區別在于,signal 只是一個信號量,只能喚醒一個等待的線程,想喚醒多個就得多次調用,而 broadcast 可以喚醒所有在等待的線程。如果沒有等待的線程,這兩個方法都沒有作用。@synchronized代碼塊
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{@synchronized(self) {sleep(2);NSLog(@"線程1");}NSLog(@"線程1解鎖成功"); });dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{sleep(1);@synchronized(self) {NSLog(@"線程2");} });@synchronized(object) 指令使用的 object 為該鎖的唯一標識,只有當標識相同時,才滿足互斥,所以如果線程 2 中的 @synchronized(self) 改為@synchronized(self.view),則線程2就不會被阻塞。@synchronized 指令實現鎖的優點就是我們不需要在代碼中顯式的創建鎖對象,便可以實現鎖的機制,但作為一種預防措施,@synchronized 塊會隱式的添加一個異常處理例程來保護代碼,該處理例程會在異常拋出的時候自動的釋放互斥鎖。 @synchronized 還有一個好處就是不用擔心忘記解鎖了。如果在 @sychronized(object){} 內部 object 被釋放或被設為 nil,從我做的測試的結果來看,的確沒有問題,但如果 object 一開始就是 nil,則失去了鎖的功能。不過雖然 nil 不行,但 @synchronized([NSNull null]) 是完全可以的。條件信號量 dispatch_semaphore_t
dispatch_semaphore 是 GCD 用來同步的一種方式,與他相關的只有三個函數,一個是創建信號量,一個是等待信號,一個是發送信號。 有點和NSCondition類似,都是一種基于信號的同步方式。但 NSCondition 信號只能發送,不能保存(如果沒有線程在等待,則發送的信號會失效)而 dispatch_semaphore 能保存發送的信號。dispatch_semaphore 的核心是 dispatch_semaphore_t 類型的信號量。dispatch_semaphore_create(long value);dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);dispatch_semaphore_signal(dispatch_semaphore_t dsema);dispatch_semaphore_t signal = dispatch_semaphore_create(1); dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{dispatch_semaphore_wait(signal, overTime);sleep(1);NSLog(@"線程1");dispatch_semaphore_signal(signal);NSLog(@"%@",signal); });dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{dispatch_semaphore_wait(signal, overTime);sleep(1);NSLog(@"線程2");dispatch_semaphore_signal(signal);NSLog(@"%@",signal); });dispatch_semaphore_wait(signal, overTime); 方法會判斷 signal 的信號值是否大于 0。大于 0 不會阻塞線程,消耗掉一個信號,執行后續任務。如果信號值為 0,該線程會和 NSCondition 一樣直接進入 waiting 狀態,等待其他線程發送信號喚醒線程去執行后續任務,或者當 overTime 時限到了,也會執行后續任務。dispatch_semaphore_signal(signal); 發送信號,如果沒有等待的線程接受信號,則使 signal 信號值加一(做到對信號的保存)。從上面的實例代碼可以看到,一個 dispatch_semaphore_wait(signal, overTime); 方法會去對應一個 dispatch_semaphore_signal(signal); 看起來像 NSLock 的 lock 和 unlock,其實可以這樣理解,區別只在于有信號量這個參數,lock unlock 只能同一時間,一個線程訪問被保護的臨界區,而如果 dispatch_semaphore 的信號量初始值為 x ,則可以有 x 個線程同時訪問被保護的臨界區。OSSpinLock 自旋鎖
OSSpinLock 是一種自旋鎖,也只有加鎖,解鎖,嘗試加鎖三個方法。和 NSLock 不同的是 NSLock 請求加鎖失敗的話,會先輪詢,但一秒過后便會使線程進入 waiting 狀態,等待喚醒。而 OSSpinLock 會一直輪詢,等待時會消耗大量 CPU 資源,不適用于較長時間的任務。__block OSSpinLock theLock = OS_SPINLOCK_INIT;dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{OSSpinLockLock(&theLock);NSLog(@"線程1");sleep(10);OSSpinLockUnlock(&theLock);NSLog(@"線程1解鎖成功");});dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{sleep(1);OSSpinLockLock(&theLock);NSLog(@"線程2");OSSpinLockUnlock(&theLock);});ThreadLockControlDemo[2856:316247] 線程1 ThreadLockControlDemo[2856:316247] 線程1解鎖成功 ThreadLockControlDemo[2856:316260] 線程2拿上面的輸出結果和上文 NSLock 的輸出結果做對比,會發現 sleep(10) 的情況,OSSpinLock 中的“線程 2”并沒有和”線程 1解鎖成功“在一個時間輸出,而 NSLock 這里是同一時間輸出,而是有一點時間間隔,所以 OSSpinLock 一直在做著輪詢,而不是像 NSLock 一樣先輪詢,再 waiting 等喚醒。5、常見問題
dispatch_release 已被廢棄(6.0)dispatch_release在6.0以后內部被改成對象釋放(release)所以 arc后都不再使用。
app啟動,系統默認創建5個線程
NSTimer
[self.timer setFireDate:[NSDate dateWithTimeIntervalSinceNow:5]];//暫停5s6、捕獲開發中子線程更新UI的邏輯
1.為什么UI要在主線程更新
因為UIKit不是線程安全的。試想下面這幾種情況:
兩個線程同時設置同一個背景圖片,那么很有可能因為當前圖片被釋放了兩次而導致應用崩潰。
兩個線程同時設置同一個UIView的背景顏色,那么很有可能渲染顯示的是顏色A,而此時在UIView邏輯樹上的背景顏色屬性為B。
兩個線程同時操作view的樹形結構:在線程A中for循環遍歷并操作當前View的所有subView,然后此時線程B中將某個subView直接刪除,這就導致了錯亂還可能導致應用崩潰。
iOS4之后蘋果將大部分繪圖的方法和諸如 UIColor 和 UIFont 這樣的類改寫為了線程安全可用,但是仍然強烈建議講UI操作保證在主線程中執行。
2.我的想法
View的更新操作 使用runtime 去替換 View 中實現 的方法 不變更實現。只是在中間插入 線程檢查操作,發現子線程就必須打印線程調用棧并觸發crash。
問題:替換哪些方法更合適? 都會涉及到哪些基礎控件需要category?
3.例子
1.創建一個UIImage的category
@implementation UIImage (demo)+(void)load {Method m1 = class_getClassMethod([UIImage class],@selector(imageNamed:));Method m2 = class_getClassMethod([UIImage class],@selector(ximageNamed:));// 開始交換方法實現method_exchangeImplementations(m1, m2); } +(UIImage *)ximageNamed:(NSString *)name {NSLog(@"進入方法-開始檢查線程");NSThread *thread = [NSThread currentThread];if (![thread isMainThread]) {NSLog(@" 當前線程不是主線程 %@",[NSThread callStackSymbols]);}return [UIImage ximageNamed:name]; } @end2.在一個視圖內實現一段UIImage的異步賦予圖片
UIImageView *img = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 300, 300)]; [self.view addSubview:img]; img.image = [UIImage imageNamed:@"networklosed"]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{img.image = [UIImage imageNamed:@"mncg_search_nor"]; }); NSLog(@"測試線程是否異步");7、參考
GCD使用三部曲之:基本用法
總結
- 上一篇: Gradle 之语言基础 Groovy
- 下一篇: How to mount HFS EFI