iOS多執行緒:『pthread、NSThread』詳盡總結

行走少年郎發表於2018-01-26

本文用來介紹 iOS 多執行緒中,pthread、NSThread 的使用方法及實現。

第一部分:pthread 的使用、其他相關方法。

第二部分:NSThread 的使用、執行緒相關用法、執行緒狀態控制方法、執行緒之間的通訊、執行緒安全和執行緒同步,以及執行緒的狀態轉換相關知識。

文中 Demo 我已放在了 Github 上,Demo 連結:傳送門

1. pthread

1.1 pthread 簡介

pthread 是一套通用的多執行緒的 API,可以在Unix / Linux / Windows 等系統跨平臺使用,使用 C 語言編寫,需要程式設計師自己管理執行緒的生命週期,使用難度較大,我們在 iOS 開發中幾乎不使用 pthread,但是還是來可以瞭解一下的。

引自 百度百科 POSIX 執行緒(POSIX threads),簡稱 Pthreads,是執行緒的 POSIX 標準。該標準定義了建立和操縱執行緒的一整套 API。在類Unix作業系統(Unix、Linux、Mac OS X等)中,都使用 Pthreads 作為作業系統的執行緒。Windows 作業系統也有其移植版 pthreads-win32。

引自 維基百科 POSIX 執行緒(英語:POSIX Threads,常被縮寫 為 Pthreads)是 POSIX 的執行緒標準,定義了建立和操縱執行緒的一套 API。 實現 POSIX 執行緒標準的庫常被稱作 Pthreads,一般用於 Unix-like POSIX 系統,如 Linux、Solaris。但是 Microsoft Windows 上的實現也存在,例如直接使用 Windows API 實現的第三方庫 pthreads-w32;而利用 Windows 的 SFU/SUA 子系統,則可以使用微軟提供的一部分原生 POSIX API。

1.2 pthread 使用方法

  1. 首先要包含標頭檔案#import <pthread.h>
  2. 其次要建立執行緒,並開啟執行緒執行任務
// 1. 建立執行緒: 定義一個pthread_t型別變數
pthread_t thread;
// 2. 開啟執行緒: 執行任務
pthread_create(&thread, NULL, run, NULL);
// 3. 設定子執行緒的狀態設定為 detached,該執行緒執行結束後會自動釋放所有資源
pthread_detach(thread);

void * run(void *param)    // 新執行緒呼叫方法,裡邊為需要執行的任務
{
    NSLog(@"%@", [NSThread currentThread]);

    return NULL;
}
複製程式碼
  • pthread_create(&thread, NULL, run, NULL); 中各項引數含義:
    • 第一個引數&thread是執行緒物件,指向執行緒識別符號的指標
    • 第二個是執行緒屬性,可賦值NULL
    • 第三個run表示指向函式的指標(run對應函式裡是需要在新執行緒中執行的任務)
    • 第四個是執行函式的引數,可賦值NULL

1.3 pthread 其他相關方法

  • pthread_create() 建立一個執行緒
  • pthread_exit() 終止當前執行緒
  • pthread_cancel() 中斷另外一個執行緒的執行
  • pthread_join() 阻塞當前的執行緒,直到另外一個執行緒執行結束
  • pthread_attr_init() 初始化執行緒的屬性
  • pthread_attr_setdetachstate() 設定脫離狀態的屬性(決定這個執行緒在終止時是否可以被結合)
  • pthread_attr_getdetachstate() 獲取脫離狀態的屬性
  • pthread_attr_destroy() 刪除執行緒的屬性
  • pthread_kill() 向執行緒傳送一個訊號

2. NSThread

NSThread 是蘋果官方提供的,使用起來比 pthread 更加物件導向,簡單易用,可以直接操作執行緒物件。不過也需要需要程式設計師自己管理執行緒的生命週期(主要是建立),我們在開發的過程中偶爾使用 NSThread。比如我們會經常呼叫[NSThread currentThread]來顯示當前的程式資訊。

下邊我們說說 NSThread 如何使用。

2.1 建立、啟動執行緒

  • 先建立執行緒,再啟動執行緒
// 1. 建立執行緒
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
// 2. 啟動執行緒
[thread start];    // 執行緒一啟動,就會線上程thread中執行self的run方法

// 新執行緒呼叫方法,裡邊為需要執行的任務
- (void)run {
     NSLog(@"%@", [NSThread currentThread]);
}
複製程式碼
  • 建立執行緒後自動啟動執行緒
// 1. 建立執行緒後自動啟動執行緒
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];

// 新執行緒呼叫方法,裡邊為需要執行的任務
- (void)run {
     NSLog(@"%@", [NSThread currentThread]);
}
複製程式碼
  • 隱式建立並啟動執行緒
// 1. 隱式建立並啟動執行緒
[self performSelectorInBackground:@selector(run) withObject:nil];

// 新執行緒呼叫方法,裡邊為需要執行的任務
- (void)run {
     NSLog(@"%@", [NSThread currentThread]);
}
複製程式碼

2.2 執行緒相關用法

// 獲得主執行緒
+ (NSThread *)mainThread;    

// 判斷是否為主執行緒(物件方法)
- (BOOL)isMainThread;

// 判斷是否為主執行緒(類方法)
+ (BOOL)isMainThread;    

// 獲得當前執行緒
NSThread *current = [NSThread currentThread];

// 執行緒的名字——setter方法
- (void)setName:(NSString *)n;    

// 執行緒的名字——getter方法
- (NSString *)name;    
複製程式碼

2.3 執行緒狀態控制方法

  • 啟動執行緒方法
- (void)start;
// 執行緒進入就緒狀態 -> 執行狀態。當執行緒任務執行完畢,自動進入死亡狀態
複製程式碼
  • 阻塞(暫停)執行緒方法
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 執行緒進入阻塞狀態
複製程式碼
  • 強制停止執行緒
+ (void)exit;
// 執行緒進入死亡狀態
複製程式碼

2.4 執行緒之間的通訊

在開發中,我們經常會在子執行緒進行耗時操作,操作結束後再回到主執行緒去重新整理 UI。這就涉及到了子執行緒和主執行緒之間的通訊。我們先來了解一下官方關於 NSThread 的執行緒間通訊的方法。

// 在主執行緒上執行操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
  // equivalent to the first method with kCFRunLoopCommonModes

// 在指定執行緒上執行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

// 在當前執行緒上執行操作,呼叫 NSObject 的 performSelector:相關方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
複製程式碼

下面通過一個經典的下載圖片 DEMO 來展示執行緒之間的通訊。具體步驟如下:

  1. 開啟一個子執行緒,在子執行緒中下載圖片。
  2. 回到主執行緒重新整理 UI,將圖片展示在 UIImageView 中。

DEMO 程式碼如下:

/**
 * 建立一個執行緒下載圖片
 */
- (void)downloadImageOnSubThread {
    // 在建立的子執行緒中呼叫downloadImage下載圖片
    [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
}

/**
 * 下載圖片,下載完之後回到主執行緒進行 UI 重新整理
 */
- (void)downloadImage {
    NSLog(@"current thread -- %@", [NSThread currentThread]);
    
    // 1. 獲取圖片 imageUrl
    NSURL *imageUrl = [NSURL URLWithString:@"https://ysc-demo-1254961422.file.myqcloud.com/YSC-phread-NSThread-demo-icon.jpg"];
    
    // 2. 從 imageUrl 中讀取資料(下載圖片) -- 耗時操作
    NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
    // 通過二進位制 data 建立 image
    UIImage *image = [UIImage imageWithData:imageData];
    
    // 3. 回到主執行緒進行圖片賦值和介面重新整理
    [self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
}

/**
 * 回到主執行緒進行圖片賦值和介面重新整理
 */
- (void)refreshOnMainThread:(UIImage *)image {
    NSLog(@"current thread -- %@", [NSThread currentThread]);
    
    // 賦值圖片到imageview
    self.imageView.image = image;
}
複製程式碼

2.5 NSThread 執行緒安全和執行緒同步

執行緒安全:如果你的程式碼所在的程式中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。

若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒安全的;若有多個執行緒同時執行寫操作(更改變數),一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。

執行緒同步:可理解為執行緒 A 和 執行緒 B 一塊配合,A 執行到一定程度時要依靠執行緒 B 的某個結果,於是停下來,示意 B 執行;B 依言執行,再將結果給 A;A 再繼續操作。

舉個簡單例子就是:兩個人在一起聊天。兩個人不能同時說話,避免聽不清(操作衝突)。等一個人說完(一個執行緒結束操作),另一個再說(另一個執行緒再開始操作)。

下面,我們模擬火車票售賣的方式,實現 NSThread 執行緒安全和解決執行緒同步問題。

場景:總共有50張火車票,有兩個售賣火車票的視窗,一個是北京火車票售賣視窗,另一個是上海火車票售賣視窗。兩個視窗同時售賣火車票,賣完為止。

2.5.1 NSThread 非執行緒安全

先來看看不考慮執行緒安全的程式碼:

/**
 * 初始化火車票數量、賣票視窗(非執行緒安全)、並開始賣票
 */
- (void)initTicketStatusNotSave {
    // 1. 設定剩餘火車票為 50
    self.ticketSurplusCount = 50;
    
    // 2. 設定北京火車票售賣視窗的執行緒
    self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
    self.ticketSaleWindow1.name = @"北京火車票售票視窗";
    
    // 3. 設定上海火車票售賣視窗的執行緒
    self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
    self.ticketSaleWindow2.name = @"上海火車票售票視窗";
    
    // 4. 開始售賣火車票
    [self.ticketSaleWindow1 start];
    [self.ticketSaleWindow2 start];

}

/**
 * 售賣火車票(非執行緒安全)
 */
- (void)saleTicketNotSafe {
    while (1) {
        //如果還有票,繼續售賣
        if (self.ticketSurplusCount > 0) {
            self.ticketSurplusCount --;
            NSLog(@"%@", [NSString stringWithFormat:@"剩餘票數:%ld 視窗:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
            [NSThread sleepForTimeInterval:0.2];
        }
        //如果已賣完,關閉售票視窗
        else {
            NSLog(@"所有火車票均已售完");
            break;
        }
    }
}
複製程式碼

執行後部分結果為:

iOS多執行緒:『pthread、NSThread』詳盡總結

可以看到在不考慮執行緒安全的情況下,得到票數是錯亂的,這樣顯然不符合我們的需求,所以我們需要考慮執行緒安全問題。

2.5.2 NSThread 執行緒安全

執行緒安全解決方案:可以給執行緒加鎖,在一個執行緒執行該操作的時候,不允許其他執行緒進行操作。iOS 實現執行緒加鎖有很多種方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/ge等等各種方式。為了簡單起見,這裡不對各種鎖的解決方案和效能做分析,只用最簡單的@synchronized來保證執行緒安全,從而解決執行緒同步問題。

考慮執行緒安全的程式碼:

/**
 * 初始化火車票數量、賣票視窗(執行緒安全)、並開始賣票
 */
- (void)initTicketStatusSave {
    // 1. 設定剩餘火車票為 50
    self.ticketSurplusCount = 50;
    
    // 2. 設定北京火車票售賣視窗的執行緒
    self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketSafe) object:nil];
    self.ticketSaleWindow1.name = @"北京火車票售票視窗";
    
    // 3. 設定上海火車票售賣視窗的執行緒
    self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketSafe) object:nil];
    self.ticketSaleWindow2.name = @"上海火車票售票視窗";
    
    // 4. 開始售賣火車票
    [self.ticketSaleWindow1 start];
    [self.ticketSaleWindow2 start];
    
}

/**
 * 售賣火車票(執行緒安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 互斥鎖
        @synchronized (self) {
            //如果還有票,繼續售賣
            if (self.ticketSurplusCount > 0) {
                self.ticketSurplusCount --;
                NSLog(@"%@", [NSString stringWithFormat:@"剩餘票數:%ld 視窗:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
                [NSThread sleepForTimeInterval:0.2];
            }
            //如果已賣完,關閉售票視窗
            else {
                NSLog(@"所有火車票均已售完");
                break;
            }
        }
    }
}
複製程式碼

執行後結果為:

iOS多執行緒:『pthread、NSThread』詳盡總結

省略一部分結果圖。。。

iOS多執行緒:『pthread、NSThread』詳盡總結

可以看出,在考慮了執行緒安全的情況下,加鎖之後,得到的票數是正確的,沒有出現混亂的情況。我們也就解決了多個執行緒同步的問題。

2.6 執行緒的狀態轉換

當我們新建一條執行緒NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];,在記憶體中的表現為:

iOS多執行緒:『pthread、NSThread』詳盡總結

當呼叫[thread start];後,系統把執行緒物件放入可排程執行緒池中,執行緒物件進入就緒狀態,如下圖所示。

iOS多執行緒:『pthread、NSThread』詳盡總結

當然,可排程執行緒池中,會有其他的執行緒物件,如下圖所示。在這裡我們只關心左邊的執行緒物件。

iOS多執行緒:『pthread、NSThread』詳盡總結

下邊我們來看看當前執行緒的狀態轉換。

  • 如果CPU現在排程當前執行緒物件,則當前執行緒物件進入執行狀態,如果CPU排程其他執行緒物件,則當前執行緒物件回到就緒狀態。
  • 如果CPU在執行當前執行緒物件的時候呼叫了sleep方法\等待同步鎖,則當前執行緒物件就進入了阻塞狀態,等到sleep到時\得到同步鎖,則回到就緒狀態。
  • 如果CPU在執行當前執行緒物件的時候執行緒任務執行完畢\異常強制退出,則當前執行緒物件進入死亡狀態。

只看文字可能不太好理解,具體當前執行緒物件的狀態變化如下圖所示。

iOS多執行緒:『pthread、NSThread』詳盡總結


iOS多執行緒詳盡總結系列文章:

待完成:

  • iOS多執行緒:『RunLoop』詳盡總結

相關文章