iOS線程知識整理

什麼是線程

一、線程的定義、狀態、屬性

進程

進程:(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中,線程是能獨立運行的基本單位,於是也是獨立調度和分派的基本單位。因爲線程很「輕」,故線程的切換很是迅速且開銷小。

③可併發執行。

在一個進程中的多個線程之間,能夠併發執行,甚至容許在一個進程中全部線程都能併發執行;一樣,不一樣進程中的線程也能併發執行。

④共享進程資源。

在同一進程中的各個線程,均可以共享該進程所擁有的資源,這首先表如今:全部線程都具備相同的地址空間(進程的地址空間),這意味着,線程能夠訪問該地址空間的每個虛地址;此外,還能夠訪問進程所擁有的已打開文件、定時器、信號量機構等。

二、線程之間的通訊

什麼是線程通訊

多個線程在處理同一個資源,而且任務不一樣時,須要線程通訊來幫助解決線程之間對同一個變量的使用或操做。就是多個線程在操做同一份數據時, 避免對同一共享變量的爭奪。

就是在一個線程進行了規定操做後,就進入等待狀態(wait), 等待其餘線程執行完他們的指定代碼事後 再將其喚醒(notify);

當咱們建立多個生產者和消費者時,沒法直到到底要喚醒哪個,因此這時候咱們就用到了notifAll()方法。

爲何要線程通訊

多個線程併發執行時, 在默認狀況下CPU是隨機切換線程的,當咱們須要多個線程來共同完成一件任務,而且咱們但願他們有規律的執行, 那麼多線程之間須要一些協調通訊,以此來幫咱們達到多線程共同操做一份數據。

固然若是咱們沒有使用線程通訊來使用多線程共同操做同一份數據的話,雖然能夠實現,可是在很大程度會形成多線程之間對同一共享變量的爭奪,那樣的話勢必爲形成不少錯誤和損失!

因此,咱們才引出了線程之間的通訊,多線程之間的通訊可以避免對同一共享變量的爭奪。

三、線程進程以及堆棧關係的總結

棧是線程獨有的,保存其運行狀態和局部自動變量的,棧在線程開始的時候初始化,每一個線程的棧相互對立,所以,棧是線程安全的,棧空間有系統管理。棧被自動分配到進程的內存空間中。後端

堆在操做系統度進程初始化的時候分配,運行過程當中也能夠向系統要額外的堆,可是用完要返還,否則就是內存泄露。安全

iOS中的線程

iOS中提供了四套多線程方案、一種一種來看。多線程

Pthreads (不作介紹)
NSThread
GCD
NSOperation & NSOperationQueue

一、NSThread

蘋果封裝、面向對象的、能夠直接操控線程對象,很是直觀和方便。可是,它的生命週期仍是須要咱們手動管理。

優缺點

優勢:輕量級

缺點:一個NSThread對象表明一個線程,須要手動管理線程的生命週期,處理線程同步等問題,線程同步對數據的加鎖會有必定的開銷。

建立並啓動

一、先建立線程類,再啓動併發

//1 建立NSThread 並啓動
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
    
[thread start];

二、建立並自動啓動app

[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];
 ////// 
__weak typeof(self) weakself = self;
[NSThread detachNewThreadWithBlock:^{
      [weakself run];
}];

三、使用 NSObject 的方法建立並自動啓動異步

[self performSelectorInBackground:@selector(run) withObject:nil];

可是在Swift中沒有這個方法:async

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 還以不少方法,下面是一些常見的方法ide

//取消線程
- (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;

二、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 : 主隊列 (主線程)
通常使用 main queue, 都是在該線程中操做 UI 相關的.也就是說, 在 main queue 中執行的任務會在主線程中執行.主線程只有一個, main queue 是與主線程相關的,因此 main queue 是串行隊列.

//Returns the default queue that is bound to the main thread.
                                                                                                                                                                       dispatch_get_main_queue(void)
{
    return DISPATCH_GLOBAL_OBJECT(dispatch_queue_t, _dispatch_main_q);                                                                                   
}
  • global queue : 全局隊列 (有多個線程)
dispatch_get_global_queue(long identifier, unsigned long flags);                                                        dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
常寫做 dispatch_get_global_queue(0, 0);

global queue 是併發隊列.能夠設置其優先級.  ???優先級問題
//@param identifier  優先級
- A quality of service class defined in qos_class_t or a priority defined in  
                                                                                                                                                   - dispatch_queue_priority_t.
 //@param flags  備用參數
- Reserved for future use. Passing any value other than zero may result in
- a NULL return value.
//@result  返回一個全局隊列
- Returns the requested global queue or NULL if the requested global queue
- does not exist.
  • custom queue : 自定義隊列 (串行:單線程 ,並行:有多個線程)
這些隊列是能夠是串行的, 也能夠是並行的。默認是串行的.
dispatch_queue_attr_t設置成NULL的時候默認表明串行。
串行隊列能夠保證任務是串行的, 保證了執行順序.相似鎖機制.
   
dispatch_queue_create(const char *_Nullable label,dispatch_queue_attr_t _Nullable attr);
                                                                                           
//@param label  隊列名稱 儘可能別重名
- A string label to attach to the queue.
- This parameter is optional and may be NULL.
//@param attr   隊列類型  默認 DISPATCH_QUEUE_SERIAL
- A predefined attribute such as DISPATCH_QUEUE_SERIAL,
- DISPATCH_QUEUE_CONCURRENT, or the result of a call to
- a dispatch_queue_attr_make_with_* function.
//@result
- The newly created dispatch 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號任務是沒法執行的
三、當任務走到同步線程開啓的時候,線程會被阻塞,直到2號任務block內的任務執行完成纔會釋放
四、但是同步線程把任務加入queue隊列以後才發現,本身要執行的這個任務前面還卡着一個1號任務
五、線程被阻塞,1號任務沒法完成,1號任務沒完成 2號任務就不能執行
六、形成死鎖

因此改一下,只要把同步任務換個隊列執行,就能夠避免死鎖了:
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 插入的任務時 同時只執行了一個任務

三、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}

一、直接執行建立的operation 默認是當前線程
二、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 來解除依賴關係。
不一樣的隊列之間的任務也能夠依賴

四、鎖

NSLock

NSLock 遵循 NSLocking 協議,

lock 方法是加鎖

unlock 是解鎖

tryLock 是嘗試加鎖,若是失敗的話返回 NO

lockBeforeDate: 是在指定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];
    //線程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        while (!array.count) {
            [lock wait];
        }
        [array removeAllObjects];
        NSLog(@"array removeAllObjects");
        [lock unlock];
    });
    
    //線程2
    dispatch_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 等喚醒。

五、常見問題

dispatch_release 已被廢棄(6.0)dispatch_release在6.0之後內部被改爲對象釋放(release)因此 arc後都再也不使用。

app啓動,系統默認建立5個線程

NSTimer

[self.timer setFireDate:[NSDate dateWithTimeIntervalSinceNow:5]];//暫停5s

六、捕獲開發中子線程更新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];
}
@end

2.在一個視圖內實現一段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(@"測試線程是否異步");

七、參考

GCD使用三部曲之:基本用法

相關文章
相關標籤/搜索