iOS 開發中的多線程

線程、進程

什麼是線程、進程

  有的人說進程就像是人的腦殼,線程就是腦殼上的頭髮~~。其實這麼比方不算錯,可是更簡單的來講,用迅雷下載文件,迅雷這個程序就是一個進程,下載的文件就是一個線程,同時下載三個文件就是多線程。一個進程能夠只包含一個線程去處理事務,也能夠有多個線程。ios

<!--more-->程序員

多線程的優勢和缺點

  多線程能夠大大提升軟件的執行效率和資源(CPU、內存)利用率,由於CPU只能夠處理一個線程(多核CPU另說),而多線程可讓CPU同時處理多個任務(其實CPU同一時間仍是隻處理一個線程,可是若是切換的夠快,就能夠了認爲同時處理多個任務)。可是多線程也有缺點:當線程過多,會消耗大量的CPU資源,並且,每開一條線程也是須要耗費資源的(iOS主線程佔用1M內存空間,子線程佔用512KB)。編程

iOS開發中的多線程

  iOS程序在啓動後會自動開啓一個線程,稱爲 主線程 或者 UI線程 ,用來顯示、刷新UI界面,處理點擊、滾動等事件,因此耗費時間的事件(好比網絡、磁盤操做)儘可能不要放在主線程,不然會阻塞主線程形成界面卡頓。
iOS開發中的多線程實現方案有四種:數組

技術方案 簡介 語言 生命週期管理
pthread 一套通用的多線程API,適用於UnixLinuxWindows等系統,跨平臺可移植,使用難度大 C 程序員管理
NSThread 使用更加面向對象,簡單易用,可直接操做線程對象 Objective-C 程序員手動實例化
GCD 旨在替代NSThread等線程技術,充分利用設備的多核 C 自動管理
NSOperation 基於GCD(底層是GCD),比GCD多了一些更簡單實用的功能,使用更加面向對象 Objective-C 自動管理

多線程中GCD我使用比較多,以GCD爲例,多線程有兩個核心概念:安全

  1. 任務 (作什麼?)網絡

  2. 隊列 (存聽任務,怎麼作?)多線程

任務就是你開闢多線程要來作什麼?而每一個線程都是要加到一個隊列中去的,隊列決定任務用什麼方式來執行。閉包

線程執行任務方式分爲:併發

  1. 異步執行app

  2. 同步執行

同步執行只能在當前線程執行,不能開闢新的線程。並且是必須、當即執行。而異步執行能夠開闢新的線程。

隊列分爲:

  1. 併發隊列

  2. 串行隊列

併發隊列可讓多個線程同時執行(必須是異步),串行隊列則是讓任務一個接一個的執行。打個比方說,串行隊列就是單車道,再多的車也得一個一個的跑(--:我倆車強行並着跑不行? --:來人,拖出去砍了!),而串行是多車道,能夠幾輛車同時並着跑。那麼究竟是幾車道?併發隊列有個最大併發數,通常能夠手動設置。

那麼,線程加入到隊列中,到底會怎麼執行?

併發隊列 串行隊列(非主隊列) 主隊列(只有主線程,串行隊列)
同步 不開啓新的線程,串行 不開啓新的線程,串行 不開啓新的線程,串行
異步 開啓新的線程,併發 開啓新的線程,串行 不開啓新的線程,串行

注意:

  1. 只用在併發隊列異步執行纔會開啓新的線程併發執行;

  2. 在當前串行隊列中開啓一個同步線程會形成 線程阻塞 ,由於上文說過,同步線程須要當即立刻執行,當在當前串行隊列中建立同步線程時須要在串行隊列當即執行任務,而此時線程還須要向下繼續執行任務,形成阻塞。

上面提到線程會阻塞,那麼什麼是阻塞?除了阻塞以外線程還有其餘什麼狀態?
通常來講,線程有五個狀態:

  • 新建狀態:線程剛剛被建立,尚未調用 run 方法,這個時候的線程就是新建狀態;

  • 就緒狀態:在新建線程被建立以後調用了 run 方法,可是CPU並非真正的同時執行多個任務,因此要等待CPU調用,這個時候線程處於就緒狀態,隨時可能進入下一個狀態;

  • 運行狀態:在線程執行過 run方法以後,CPU已經調度該線程即線程獲取了CPU時間;

  • 阻塞狀態:線程在運行時可能會進入阻塞狀態,好比線程睡眠(sleep);但願獲得一個鎖,可是該鎖正被其餘線程擁有。。

  • 死亡狀態:當線程執行完任務或者由於異常狀況提早終止了線程

iOS開發中的多線程的使用

pthread的使用

使用下面代碼能夠建立一個線程:

int pthread_create(pthread_t * __restrict, const pthread_attr_t * __restrict,void *(*)(void *), void * __restrict)

能夠看到這個方法有四個參數,主要參數有 pthread_t __restrict ,由於該方法是C語言,因此這個參數不是一個對象,而是一個 pthread_t 的地址,還有 void ()(void ) 是一個無返回值的函數指針。
使用代碼:

void * run(void *param)
{
    NSLog(@"currentThread--%@", [NSThread currentThread]);
    return NULL;
}

- (void)createThread{
    pthread_t thread;
    pthread_create(&thread, NULL, run, NULL);
}

控制檯輸出:

currentThread--<NSThread: 0x7fff38602fb0>{number = 2, name = (null)}

number = 1 的線程是主線程,不爲一的時候都是子線程。

NSThread的使用

NSThread建立線程通常有三種方式:

// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
//
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
//
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument
  1. 前兩種建立以後會自動執行,第三種方式建立後須要手動執行;

  2. 第一種建立方式是建立一個子線程,相似的 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString > )array 方法能夠建立併發任務在主線程中執行,- (void)performSelector:(SEL)aSelector onThread:(NSThread )thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString > *)array 能夠選擇在哪一個線程中執行。

示例代碼:

- (void)createThread{
    // 建立線程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"我是參數"];
    thread.name = @"我是線程名字啊";
    // 啓動線程
    [thread start];
    // 或者 [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"我是參數"];
    // 或者 [self performSelectorInBackground:@selector(run:) withObject:@"我是參數"];
}
- (void)run:(NSString *)param{
    NSLog(@"-----run-----%@--%@", param, [NSThread currentThread]);
}

控制檯輸出:

-----run-----我是參數--<NSThread: 0x7ff8a2f0c940>{number = 2, name = 我是線程名字啊}

GCD的使用

蘋果官方對GCD說:

開發者要作的只是定義執行的任務並追加到適當的 Dispatch Queue 中。

在GCD中咱們要作的只是兩件事:定義任務;把任務加到隊列中。

dispatch_queue_create 獲取/建立隊列

GCD 的隊列有兩種:

Dispatch Queue 種類 說明
Serial Dispatch Queue 等待如今執行中處理結束(串行隊列)
Concurrent Dispatch Queue 不等待如今執行中處理結束(並行隊列)

GCD中的隊列都是 dispatch_queue_t 類型,獲取/建立方法:

// 1. 手動建立隊列
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
// 1.1 建立串行隊列
    dispatch_queue_t queue = dispatch_queue_create("com.sanyucz.queue", DISPATCH_QUEUE_SERIAL);
// 1.2 建立並行隊列
    dispatch_queue_t queue = dispatch_queue_create("com.sanyucz.queue", DISPATCH_QUEUE_CONCURRENT);
// 2. 獲取系統標準提供的 Dispatch Queue
// 2.1 獲取主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
// 2.2 獲取全局併發隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

須要說明的是,手動建立隊列時候的兩個關鍵參數,const char *label 指定隊列名稱,最好起一個有意義的名字,固然若是你想調試的時候刺激一下,也能夠設置爲 NULL,而 dispatch_queue_attr_t attr 參數文檔有說明:

/*!
 * @const DISPATCH_QUEUE_SERIAL
 * @discussion A dispatch queue that invokes blocks serially in FIFO order.
 */
#define DISPATCH_QUEUE_SERIAL NULL
/*!
 * @const DISPATCH_QUEUE_CONCURRENT
 * @discussion A dispatch queue that may invoke blocks concurrently and supports
 * barrier blocks submitted with the dispatch barrier API.
 */
#define DISPATCH_QUEUE_CONCURRENT \
        DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t, \
        _dispatch_queue_attr_concurrent)
  • DISPATCH_QUEUE_SERIAL 建立串行隊列按順序FIFO(First-In-First-On)先進先出;

  • DISPATCH_QUEUE_CONCURRENT 則會建立併發隊列

dispatch_async/dispatch_sync 建立任務

建立完隊列以後就是定義任務了,有兩種方式:

// 建立一個同步執行任務
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
// 建立一個異步執行任務
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

完整的示例代碼:

dispatch_queue_t queue = dispatch_queue_create("com.sanyucz.queue.asyncSerial", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
   NSLog(@"異步 + 串行 - %@",[NSThread currentThread]);
});

dispatch group 任務組

咱們可能在實際開發中會遇到這樣的需求:在兩個任務完成後再執行某一任務。雖然這種狀況能夠用串行隊列來解決,可是咱們有更加高效的方法。

直接上代碼,在代碼的註釋中講解:

// 獲取全局併發隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 建立任務組
// dispatch_group_t :A group of blocks submitted to queues for asynchronous invocation
dispatch_group_t group = dispatch_group_create();
// 在任務組中添加一個任務
dispatch_group_async(group, queue, ^{
    //
});
// 在任務組中添加另外一個任務
dispatch_group_async(group, queue, ^{
    //
});
// 當任務組中的任務執行完畢以後再執行一下任務
dispatch_group_notify(group, queue, ^{
   //
});

dispatch_barrier_async

從字面意思就能夠看出來這個變量的用處,即阻礙任務執行,它並非阻礙某一個任務的執行,而是在代碼中,在它以前定義的任務會比它先執行,在它以後定義的任務則會在它執行完以後在開始執行。就像一個欄柵。

使用代碼:

dispatch_queue_t queue = dispatch_queue_create("com.gcd.barrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
   NSLog(@"----1-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
   NSLog(@"----2-----%@", [NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
   NSLog(@"----barrier-----%@", [NSThread currentThread]);
}); 
dispatch_async(queue, ^{
   NSLog(@"----3-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
   NSLog(@"----4-----%@", [NSThread currentThread]);
});

控制檯輸出:

----1-----<NSThread: 0x7fdc60c0fd90>{number = 2, name = (null)}
----2-----<NSThread: 0x7fdc60c11500>{number = 3, name = (null)}
----barrier-----<NSThread: 0x7fdc60c11500>{number = 3, name = (null)}
----3-----<NSThread: 0x7fdc60c11500>{number = 3, name = (null)}
----4-----<NSThread: 0x7fdc60c0fd90>{number = 2, name = (null)}

dispatch_apply 遍歷執行任務

dispatch_apply 的用法相似於對數組元素進行 for循環 遍歷,可是 dispatch_apply 的遍歷是無序的。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 10; i++) {
   [array addObject:@(i)];
}
// array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
NSLog(@"--------apply begin--------");
dispatch_apply(array.count, queue, ^(size_t index) {
   NSLog(@"%@---%zu", [NSThread currentThread], index);
});
NSLog(@"--------apply done --------");

控制檯輸出:

--------apply begin--------
<NSThread: 0x7ffa7bd05800>{number = 1, name = main}---0
<NSThread: 0x7ffa7bd05800>{number = 1, name = main}---4
<NSThread: 0x7ffa7bd05800>{number = 1, name = main}---5
<NSThread: 0x7ffa7bda77c0>{number = 2, name = (null)}---1
<NSThread: 0x7ffa7be1fd00>{number = 4, name = (null)}---3
<NSThread: 0x7ffa7bd05800>{number = 1, name = main}---6
<NSThread: 0x7ffa7be1a920>{number = 3, name = (null)}---2
<NSThread: 0x7ffa7bd05800>{number = 1, name = main}---8
<NSThread: 0x7ffa7be1fd00>{number = 4, name = (null)}---9
<NSThread: 0x7ffa7bda77c0>{number = 2, name = (null)}---7
--------apply done --------

能夠看到,遍歷的時候自動開啓多線程,能夠無序併發執行多個任務,可是有一點能夠肯定,就是 NSLog(@"--------apply done --------"); 這段代碼必定是在全部任務執行完以後纔會去執行。

GCD 的其餘用法

除了上面的那些,GCD還有其餘的用法

  • dispatch_after 延期執行任務

  • dispatch_suspend / dispatch_resume 暫停/恢復某一任務

  • dispatch_once 保證代碼只執行一次,並且線程安全

  • Dispatch I/O 能夠以更小的粒度讀寫文件

NSOperation的使用

NSOperation 及其子類

NSOperationNSOperationQueue 配合使用也能實現併發多線程,可是須要注意的是 NSOperation 是個抽象類,想要封裝操做須要使用其子類。
系統爲咱們提供了兩個子類:

  • NSInvocationOperation

  • NSBlockOperation

固然,咱們也能夠自定義其子類,只是須要重寫 main() 方法。

先看下系統提供兩個子類的初始化方法:

- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;
+ (instancetype)blockOperationWithBlock:(void (^)(void))block;

兩個子類初始化方法不同的地方就是一個用 實例對象方法選擇器 來肯定執行一個方法,另一個是用block閉包保存執行一段代碼塊。
另外 NSBlockOperation 還有一個實例方法 - (void)addExecutionBlock:(void (^)(void))block; ,只要調用這個方法以致於封裝的操做數大於一個就會開啓新的線程異步操做。
最後調用NSOperationstart方法啓動任務。

NSOperationQueue

NSOperation 默認是執行同步任務,可是咱們能夠把它加入到 NSOperationQueue 中編程異步操做。

- (void)addOperation:(NSOperation *)op;
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;
- (void)addOperationWithBlock:(void (^)(void))block;

以前提到過多線程併發隊列能夠設置最大併發數,以及隊列的取消、暫停、恢復操做:

// 建立隊列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
// 設置最大併發操做數
queue.maxConcurrentOperationCount = 2; // 併發隊列
queue.maxConcurrentOperationCount = 1; // 串行隊列

 // 恢復隊列,繼續執行
queue.suspended = NO;
// 暫停(掛起)隊列,暫停執行
queue.suspended = YES;

// 取消隊列
[queue cancelAllOperations];

線程安全

多線程使用的時候,可能會多條線程同時訪問/賦值某一變量,如不加限制的話多相處同時訪問會出問題。具體狀況能夠搜索一下相關資料,多線程的 買票問題 非常經典。
iOS線程安全解決方法通常有如下幾種:

  • @synchronized 關鍵字

  • NSLock 對象

  • NSRecursiveLock 遞歸鎖

  • GCD (dispatch_sync 或者 dispatch_barrier_async)

在iOS中線程安全問題通常是關鍵字 @synchronized 用加鎖來完成。
示例代碼:

@synchronized(self) {
      // 這裏是安全的,同一時間只有一個線程能到這裏哦~~      
}

須要注意的是 synchronized 後面括號裏的 self 是個 token ,該 token 不能使用局部變量,應該是全局變量或者在線程併發期間一直存在的對象。由於線程判斷該加鎖的代碼有沒有線程在訪問是經過該 token 來肯定的。

首發於https://iosgg.cn/2016/05/22/multithreading_iOS/

相關文章
相關標籤/搜索