iOS多線程:『pthread、NSThread』詳盡總結

本文首發於個人我的博客: 『不羈閣』 https://bujige.net
文章連接: https://bujige.net/blog/iOS-Complete-learning-pthread-and-NSThread.html
本文更新:2018年01月26日13:42:11
本文用來介紹 iOS 多線程中, pthread、NSThread 的使用方法及實現。
第一部分:pthread 的使用、其餘相關方法。
第二部分:NSThread 的使用、線程相關用法、線程狀態控制方法、線程之間的通訊、線程安全和線程同步,以及線程的狀態轉換相關知識。
文中 Demo 我已放在了 Github 上,Demo 連接: 傳送門

1. pthread

1.1 pthread 簡介

pthread 是一套通用的多線程的 API,能夠在Unix / Linux / Windows 等系統跨平臺使用,使用 C 語言編寫,須要程序員本身管理線程的生命週期,使用難度較大,咱們在 iOS 開發中幾乎不使用 pthread,可是仍是來能夠了解一下的。html

引自 百度百科
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。git

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 如何使用。github

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 線程安全和線程同步

線程安全:若是你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。oop

若每一個線程中對全局變量、靜態變量只有讀操做,而無寫操做,通常來講,這個全局變量是線程安全的;如有多個線程同時執行寫操做(更改變量),通常都須要考慮線程同步,不然的話就可能影響線程安全。性能

線程同步:可理解爲線程 A 和 線程 B 一塊配合,A 執行到必定程度時要依靠線程 B 的某個結果,因而停下來,示意 B 運行;B 依言執行,再將結果給 A;A 再繼續操做。ui

舉個簡單例子就是:兩我的在一塊兒聊天。兩我的不能同時說話,避免聽不清(操做衝突)。等一我的說完(一個線程結束操做),另外一個再說(另外一個線程再開始操做)。

下面,咱們模擬火車票售賣的方式,實現 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;
}
}
}

運行後部分結果爲:

YSC-phread-NSThread-demo-NotSave.png

能夠看到在不考慮線程安全的狀況下,獲得票數是錯亂的,這樣顯然不符合咱們的需求,因此咱們須要考慮線程安全問題。

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;
}
}
}
}

運行後結果爲:

YSC-phread-NSThread-demo-Save.png

省略一部分結果圖。。。

YSC-phread-NSThread-demo-Save1.png

能夠看出,在考慮了線程安全的狀況下,加鎖以後,獲得的票數是正確的,沒有出現混亂的狀況。咱們也就解決了多個線程同步的問題。

2.6 線程的狀態轉換

當咱們新建一條線程NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; ,在內存中的表現爲:

YSC-phread-NSThread-demo-StatusChange.png

當調用[thread start];後,系統把線程對象放入可調度線程池中,線程對象進入就緒狀態,以下圖所示。

YSC-phread-NSThread-demo-StatusChange1.png

固然,可調度線程池中,會有其餘的線程對象,以下圖所示。在這裏咱們只關心左邊的線程對象。

YSC-phread-NSThread-demo-StatusChange2.png

下邊咱們來看看當前線程的狀態轉換。

  • 若是CPU如今調度當前線程對象,則當前線程對象進入運行狀態,若是CPU調度其餘線程對象,則當前線程對象回到就緒狀態。
  • 若是CPU在運行當前線程對象的時候調用了sleep方法等待同步鎖,則當前線程對象就進入了阻塞狀態,等到sleep到時獲得同步鎖,則回到就緒狀態。
  • 若是CPU在運行當前線程對象的時候線程任務執行完畢異常強制退出,則當前線程對象進入死亡狀態。

只看文字可能不太好理解,具體當前線程對象的狀態變化以下圖所示。

YSC-phread-NSThread-demo-StatusChange3.png


iOS多線程詳盡總結系列文章:

相關文章
相關標籤/搜索