iOS 多線程:[pthread,NSThread]詳細總結(jié)

image.png

本文主要用來介紹iOS 多線程中,pthread,NSThread 的使用方法及實現(xiàn)
第一部分:pthread 的使用,其他相關(guān)方法。
第二部分:NSThread 的使用,線程相關(guān)用法,線程狀態(tài)控制方法,線程之間的通信,線程安全和線程同步,以及線程的狀態(tài)轉(zhuǎn)換等相關(guān)知識。

1.pthread

1.1 pthread簡介

pthread 是一套通用的多線程的 API,可以在Unix / Linux / Windows 等系統(tǒng)跨平臺使用,使用 C 語言編寫,需要程序員自己管理線程的生命周期,使用難度較大,我們在 iOS 開發(fā)中幾乎不使用 pthread,但是還是來可以了解一下的。

引自 百度百科
POSIX線程(POSIX threads),簡稱Pthreads,是線程的POSIX標(biāo)準(zhǔn)。該標(biāo)準(zhǔn)定義了創(chuàng)建和操縱線程的一整套API。在類Unix操作系統(tǒng)(Unix、Linux、Mac OS X等)中,都使用Pthreads作為操作系統(tǒng)的線程。Windows操作系統(tǒng)也有其移植版pthreads-win32

1.2 pthread使用方法
  1. 首先要包含頭文件#import<pthread.h>
  2. 其次要創(chuàng)建線程,并開啟線程執(zhí)行任務(wù)。
// 1. 創(chuàng)建線程: 定義一個pthread_t類型變量
pthread_t thread;
// 2. 開啟線程: 執(zhí)行任務(wù)
pthread_create(&thread, NULL, run, NULL);
// 3. 設(shè)置子線程的狀態(tài)設(shè)置為 detached,該線程運行結(jié)束后會自動釋放所有資源
pthread_detach(thread);

void * run(void *param)    // 新線程調(diào)用方法,里邊為需要執(zhí)行的任務(wù)
{
    NSLog(@"%@", [NSThread currentThread]);

    return NULL;
}
  • pthread_create(&thread, NULL, run, NULL)中各項參數(shù)含義
    • 第一個參數(shù)&thread是線程對象,指向線程標(biāo)識符的指針
    • 第二個是線程屬性,可賦值NULL
    • 第三個run表示指向函數(shù)的指針(run 對應(yīng)函數(shù)里是需要再新線程中執(zhí)行的任務(wù))
    • 第四個是運行函數(shù)的參數(shù),可賦值為 NULL
1.3 pthread 其他相關(guān)方法
  • pthread_create() 創(chuàng)建一個線程
  • pthread_exit() 終止當(dāng)前線程
  • pthread_cancel() 中斷另外一個線程的運行
  • pthread_join() 阻塞當(dāng)前的線程,直到另外一個線程運行結(jié)束
  • pthread_attr_init() 初始化線程的屬性
  • pthread_attr_setdetachstate() 設(shè)置線程脫離狀態(tài)的屬性(決定這個線程在終止時是否可以被結(jié)合)
  • pthread_attr_getdetachstate() 獲取脫離狀態(tài)的屬性
  • pthread_attr_destory() 刪除線程的屬性
  • pthread_kill() 向線程發(fā)送一個信號

2. NSThread

NSThread 是蘋果官方提供的,使用起來比 pthread 更加面向?qū)ο螅唵我子茫梢灾苯硬僮骶€程對象。不過也需要需要程序員自己管理線程的生命周期(主要是創(chuàng)建),我們在開發(fā)的過程中偶爾使用 NSThread。比如我們會經(jīng)常調(diào)用[NSThread currentThread]來顯示當(dāng)前的進程信息。

接下來我們了解一下 NSThread 如何使用

2.1 先創(chuàng)建線程,再啟動線程
  • 先創(chuàng)建線程,再啟動線程
- (void)createThread {
    // 創(chuàng)建線程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    // 啟動線程
    [thread start];
}

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

運行結(jié)果

image.png
  • 創(chuàng)建線程后自動啟動線程
- (void)createAutoThread {
    // 創(chuàng)建線程后自動啟動線程
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}

運行結(jié)果

image.png
  • 隱式創(chuàng)建并啟動線程
- (void)createPrivacyThread {
    // 隱式創(chuàng)建并啟動線程
    [self performSelector:@selector(run) withObject:nil];
}

運行結(jié)果

image.png
2.2 線程相關(guān)方法
  • mainThread 獲取主線程
  • isMainThread 判斷是否為主線程(對象方法,類方法)
  • [NSThread currentThread]獲得當(dāng)前線程
  • setName 設(shè)置線程的名字
  • name 獲取線程的名字
2.3 線程狀態(tài)控制方法
  • 啟動線程方法
// 線程進入就緒狀態(tài) -> 運行狀態(tài)。當(dāng)線程任務(wù)執(zhí)行完畢,自動進入死亡狀態(tài)
- (void)start;
  • 阻塞(暫停)線程方法
// 線程進入阻塞狀態(tài) 
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
  • 強制停止線程
// 線程進入死亡狀態(tài)
+ (void)exit;
2.4 線程之間的通信

在開發(fā)中,我們經(jīng)常會在子線程進行耗時操作,操作結(jié)束后再回到主線程去刷新 UI。這就涉及到了子線程和主線程之間的通信。我們先來了解一下官方關(guān)于 NSThread 的線程間通信的方法。

// 在主線程上執(zhí)行操作
- (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

// 在指定線程上執(zhí)行操作
- (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);

// 在當(dāng)前線程上執(zhí)行操作,調(diào)用 NSObject 的 performSelector:相關(guān)方法
- (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 中
/**
 創(chuàng)建一個線程下載圖片
 */
- (void)downloadImageOnSubThread {
    // 在創(chuàng)建的子線程中調(diào)用downloadImage下載圖片
    [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
}

/**
 下載圖片操作
 */
- (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 中讀取數(shù)據(jù)(下載圖片) -- 耗時操作
    NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
    // 通過二進制 data 創(chuàng)建 image
    UIImage *image = [UIImage imageWithData:imageData];
    
    // 3. 回到主線程進行圖片賦值和界面刷新
    [self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
}

/**
 回到主線程刷新圖片

 @param image 圖片
 */
- (void)refreshOnMainThread:(UIImage *)image {
    NSLog(@"current thread -- %@", [NSThread currentThread]);
    
    // 賦值圖片到imageview
    _imgView.image = image;
}
2.5 NSThread 線程安全和線程同步

線程安全:如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結(jié)果和單線程運行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線程安全的。

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

線程同步:可理解為線程 A 和 線程 B 一塊配合,A 執(zhí)行到一定程度時要依靠線程 B 的某個結(jié)果,于是停下來,示意 B 運行;B 依言執(zhí)行,再將結(jié)果給 A;A 再繼續(xù)操作。

下面,我們模擬火車票售賣的方式,實現(xiàn) NSThread 線程安全和解決線程同步問題。

場景:總共有50張火車票,有兩個售賣火車票的窗口,一個是廣州火車票售賣窗口,另一個是龍巖火車票售賣窗口。兩個窗口同時售賣火車票,賣完為止。

2.5.1 NSThread 非線程安全

先看看不安全的代碼

/**
 初始化火車票數(shù)量、賣票窗口(非線程安全)、并開始賣票
 */
- (void)initTicketStatusNotSave {
    _ticketSurplusCount = 50;
    
    // 設(shè)置廣州窗口賣票線程
    NSThread *ticketSaleWindow1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
    ticketSaleWindow1.name = @"廣州火車票售票窗口";
    
    // 設(shè)置龍巖窗口賣票線程
    NSThread *ticketSaleWindow2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
    ticketSaleWindow2.name = @"龍巖火車票售票窗口";
    
    // 開始售賣火車票
    [ticketSaleWindow1 start];
    [ticketSaleWindow2 start];
}

/**
 售賣火車票(非線程安全)
 */
- (void)saleTicketNotSafe {
    while (1) {
        //如果還有票,繼續(xù)售賣
        if (_ticketSurplusCount > 0) {
            _ticketSurplusCount --;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票數(shù):%d 窗口:%@", _ticketSurplusCount, [NSThread currentThread].name]);
            [NSThread sleepForTimeInterval:0.2];
        } else {  //如果已賣完,關(guān)閉售票窗口
            NSLog(@"所有火車票均已售完");
            break;
        }
    }
}

運行結(jié)果

image.png

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

2.5.2 NSThread 線程安全

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

考慮線程安全代碼

/**
 初始化火車票數(shù)量、賣票窗口(線程安全)、并開始賣票
 */
- (void)initTicketStatusSave {
    _ticketSurplusCount = 50;
    
    // 設(shè)置廣州窗口賣票線程
    NSThread *ticketSaleWindow1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicketSafe) object:nil];
    ticketSaleWindow1.name = @"廣州火車票售票窗口";
    
    // 設(shè)置龍巖窗口賣票線程
    NSThread *ticketSaleWindow2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicketSafe) object:nil];
    ticketSaleWindow2.name = @"龍巖火車票售票窗口";
    
    // 開始售賣火車票
    [ticketSaleWindow1 start];
    [ticketSaleWindow2 start];
}

/**
 售賣火車票(非線程安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 互斥鎖
        @synchronized(self) {
            //如果還有票,繼續(xù)售賣
            if (_ticketSurplusCount > 0) {
                _ticketSurplusCount --;
                NSLog(@"%@", [NSString stringWithFormat:@"剩余票數(shù):%d 窗口:%@", _ticketSurplusCount, [NSThread currentThread].name]);
                [NSThread sleepForTimeInterval:0.2];
            } else {  //如果已賣完,關(guān)閉售票窗口
                NSLog(@"所有火車票均已售完");
                break;
            }
        }
    }
}

運行結(jié)果


safe.png

可以看出,在考慮了線程安全的情況下,加鎖之后,得到的票數(shù)是正確的,沒有出現(xiàn)混亂的情況,即解決了多個線程同步的問題。

2.6 線程的狀態(tài)轉(zhuǎn)換

當(dāng)我們新建一條線程NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];在內(nèi)存中的表現(xiàn)為:

image.png

當(dāng)調(diào)用[thread start];后,系統(tǒng)把線程對象放入可調(diào)度線程池中,線程對象進入就緒狀態(tài),如下圖所示。

image.png

當(dāng)然,可調(diào)度線程池中,會有其他的線程對象,如下圖所示。在這里我們只關(guān)心左邊的線程對象。

image.png

下面我們來看看當(dāng)前線程的狀態(tài)轉(zhuǎn)換

  • 如果CPU現(xiàn)在調(diào)度當(dāng)前線程對象,則當(dāng)前線程對象進入運行狀態(tài),如果CPU調(diào)度其他線程對象,則當(dāng)前線程對象回到就緒狀態(tài)。
  • 如果CPU在運行當(dāng)前線程對象的時候調(diào)用了sleep方法\等待同步鎖,則當(dāng)前線程對象就進入了阻塞狀態(tài),等到sleep到時\得到同步鎖,則回到就緒狀態(tài)。
  • 如果CPU在運行當(dāng)前線程對象的時候線程任務(wù)執(zhí)行完畢\異常強制退出,則當(dāng)前線程對象進入死亡狀態(tài)。

當(dāng)前線程對象的狀態(tài)變化如下圖所示。

image.png

iOS 多線程詳細總結(jié)系列文章
iOS GCD之dispatch_semaphore(信號量)
iOS 多線程-GCD 詳細總結(jié)
iOS 多線程: [NSOperation NSOperationQueue] 詳解


本文參考iOS多線程:『pthread、NSThread』詳盡總結(jié),非常感謝該作者。


項目源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容