iOS多執行緒NSThread篇

Perfect_Dream發表於2017-12-19

本篇涉及內容

  • NSThread建立方式
  • NSThread常用屬性與API
  • 類方法
  • 例項方法
  • NSObject擴充套件
  • 屬性
  • 相關通知
  • 鎖(互斥鎖、自旋鎖、讀寫鎖、條件鎖)的基礎釋義
  • 鎖(互斥鎖、自旋鎖、讀寫鎖、條件鎖)與NSThread的配合使用

NSThread

  • 基於thread封裝,新增物件導向概念,效能較差,偏向底層
  • 相對於GCD和NSOperation來說是較輕量級的執行緒開發
  • 使用比較簡單,但是需要手動管理建立執行緒的生命週期、同步、非同步、加鎖等問題

本篇文章將介紹一些NSThread的常規使用方法,對底層實現有興趣的同學可以自行google

一. 屬性與API

NSThread建立API

NSThread目前有四種方法,在iOS10以前只有以下兩種:

// iOS10以前的類方法
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;  
// iOS10以前的例項方法
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);  

複製程式碼

在iOS10的時候蘋果新出了兩種建立方法:

// iOS10以後出的類方法
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
// iOS10以後出的例項方法
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
複製程式碼

四種建立方式使用程式碼如下:

// iOS10之前NSThread兩種方法建立執行緒。如下:
- (void)createFirstThreadBeforeiOS10 {
    //第一種建立方式 例項方法 最多可以傳一個引數
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [thread1 setName:@"fisrt thread"];
    NSLog(@"create first thread");
    //啟動執行緒
    [thread1 start];
    NSLog(@"start first thread");
}

- (void)createSecondThreadBeforeiOS10 {
    // 第二種建立方式 類方法。此方法不會反悔NSThread物件,建立完畢後直接啟動執行緒
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}

// iOS 10又新增了兩個方法建立執行緒。
// 需要iOS 10 才可以執行下面兩個方法。否則會出錯。
- (void)createThirdThreadBeforeiOS10 {
    //第三種建立方式 例項方法
    NSThread *thread3 = [[NSThread alloc] initWithBlock:^{
        for(int i=0;i<10;i++){
            NSLog(@"%@,第三種 i=%d",[NSThread currentThread],i);
        }
    }];
    thread3.name = @"triple kill";
    //呼叫start方法啟動執行緒
    [thread3 start];
}

- (void)createFourthThreadBeforeiOS10 {
    //第四種建立方式 類方法 此方法也不會返回NSThread物件,直接啟動執行緒
    [NSThread detachNewThreadWithBlock:^{
        for(int i=0;i<10;i++){
            NSLog(@"%@,第四種 i=%d",[NSThread currentThread],i);
        }
    }];
}
複製程式碼

執行緒建立完畢時並不會立即執行,使用類方法建立或者呼叫- (void)start;方法只是將執行緒加入可排程執行緒池,至於什麼時候執行需要等待CPU的排程。

NSThread其他API,建立方法上邊已經說過,下述所有將跳過建立方法:

以下是NSThread的類方法

  • 獲得主執行緒
+ (NSThread *)mainThread;
使用示例:
NSThread *myMainThread=[NSThread mainThread];
複製程式碼
  • 判斷當前執行緒是否是主執行緒
+ (BOOL)isMainThread;
使用示例:
BOOL isMain = [NSThread isMainThread];
複製程式碼
  • 判斷當前執行緒是否是多執行緒
+ (BOOL)isMultiThreaded;
使用示例:
BOOL isMulti = [NSThread isMultiThreaded];
複製程式碼
  • 當前執行緒休眠到指定日期
+ (void)sleepUntilDate:(NSDate *)date;
使用示例:
NSDate *myDate = [NSDate dateWithTimeInterval:5 sinceDate:[NSDate date]];
[NSThread sleepUntilDate:myDate];
複製程式碼
  • 當前執行緒休眠指定時常
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
使用示例:
[NSThread sleepForTimeInterval:5];
複製程式碼
  • 強行退出當前執行緒
+ (void)exit;
使用示例:
[NSThread exit];
複製程式碼
  • 獲取當前執行緒執行緒優先順序
+ (double)threadPriority;
使用示例:
double dPriority=[NSThread threadPriority];
複製程式碼
  • 給當前執行緒設定優先順序,排程優先順序的取值範圍是0.0 ~ 1.0,預設0.5,值越大,優先順序越高。
+ (BOOL)setThreadPriority:(double)p;
使用示例:
BOOL isSetting=[NSThread setThreadPriority:(0.0~1.0)];
複製程式碼
  • 執行緒的呼叫都會有函式的呼叫,函式的呼叫就會有棧返回地址的記錄,在這裡返回的是函式呼叫返回的虛擬地址,說白了就是在該執行緒中函式呼叫的虛擬地址的陣列
+ (NSArray *)callStackReturnAddresses;
使用示例:
NSArray * addressArray = [NSThread callStackReturnAddresses];
複製程式碼
  • 同上面的方法一樣,只不過返回的是該執行緒呼叫函式的名字數字
+ (NSArray *)callStackSymbols;
使用示例:
NSArray * nameNumArray = [NSThread callStackSymbols];
複製程式碼

注意:callStackReturnAddresscallStackSymbols這兩個函式可以同NSLog聯合使用來跟蹤執行緒的函式呼叫情況,是程式設計除錯的重要手段

以下是NSThread的例項方法

  • 判斷是否為主執行緒
- (BOOL)isMainThread; // 是否為主執行緒 
使用示例:
BOOL isMain=[tempThread isMainThread];
複製程式碼
  • 設定執行緒名稱
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
使用示例:
tempThread.name = @"測試執行緒名稱";
[tempThread setName=@"測試執行緒名稱"];
複製程式碼
  • 取消執行緒
- (void)cancel ;
使用示例:
[tempThread cancel];
複製程式碼
  • 將執行緒加入 可排程執行緒池
- (void)start ;
使用示例:
[tempThread start];
複製程式碼
  • 執行緒初始函式 執行start方法後會自動呼叫main方法。 main是預設的初始化和呼叫selector的方法。如果要繼承NSThread,可以重寫main方法來執行新執行緒的主要部分。重寫的mian方法不需要呼叫super。不要直接呼叫mian方法,而是通過start方法來呼叫。
- (void)main ;
使用示例:[tempThread main];
複製程式碼
  • 判斷執行緒是否正在執行
- (void)isExecuting;
使用示例:
BOOL isRunning = [tempThread isExecuting];
複製程式碼
  • 判斷執行緒是否已經結束
- (void)isFinished;
使用示例:
BOOL isEnd=[exampleThread isFinished];
複製程式碼
  • 判斷執行緒是否撤銷
- (void)isCancelled;
使用示例:
isCancel = [tempThread isCancelled];
複製程式碼

接下來是NSThread針對NSObject的擴充套件

  • 在主執行緒執行方法
// 在主執行緒上執行函式,wait表示是否阻塞該方法,等待主執行緒空閒再運,modes表示執行模式kCFRunLoopCommonModes
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
使用示例:
[self performSelectorOnMainThread:@Selector(要執行的函式) withObject:@"想要傳入的引數" waitUntilDone:YES/NO modes:array];
複製程式碼
  • 在主執行緒執行方法
// 作用與上一下函式相同,但是無法設定執行模式
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
使用示例:
[selfperformSelectorOnMainThread:@Selector(要執行的函式) withObject:@"想要傳入的引數" waitUntilDone:(BOOL)wait];
複製程式碼
  • 在指定執行緒執行方法
// 在指定執行緒上執行函式,wait表示是否阻塞該方法,等待指定執行緒空閒再執行,modes表示執行模式
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array 
使用示例:
[self performSelector:@Selector(要執行的函式) onThread:(自己指定的執行緒)withObject:@"想要傳入的引數" waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array];
複製程式碼
  • 在指定執行緒執行方法
// 作用與上一下函式相同,但是無法設定執行模式
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
使用示例:
[self performSelector:@Selector(要執行的函式) onThread:(自己指定的執行緒)withObject:@"想要傳入的引數" waitUntilDone:(BOOL)wait];
複製程式碼
  • 隱式建立一個執行緒並執行
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
使用示例:
[self performSelectorInBackground:@Selector(要執行的函式) withObject:@"想要傳入的引數"];
複製程式碼

NSthread的屬性

@property (readonly, retain) NSMutableDictionary *threadDictionary;//執行緒字典
@property (nullable, copy) NSString *name;執行緒名稱
@property NSUInteger stackSize ;//執行緒使用棧區大小,預設是512K
@property (readonly, getter=isExecuting) BOOL executing;//執行緒正在執行
@property (readonly, getter=isFinished) BOOL finished;//執行緒執行結束
@property (readonly, getter=isCancelled) BOOL cancelled;//執行緒是否可以取消
@property double threadPriority ; //優先順序
@property NSQualityOfService qualityOfService ; // 執行緒優先順序
          NSQualityOfServiceUserInteractive:   // 最高優先順序,主要用於提供互動UI的操作,比如處理點選事件,繪製影象到螢幕上
          NSQualityOfServiceUserInitiated:     // 次高優先順序,主要用於執行需要立即返回的任務
          NSQualityOfServiceDefault:           // 預設優先順序,當沒有設定優先順序的時候,執行緒預設優先順序
          NSQualityOfServiceUtility:           // 普通優先順序,主要用於不需要立即返回的任務
          NSQualityOfServiceBackground:        // 後臺優先順序,用於完全不緊急的任務
複製程式碼

NSThread相關的通知

NSWillBecomeMultiThreadedNotification:由當前執行緒派生出第一個其他執行緒時傳送,一般一個執行緒只傳送一次
NSDidBecomeSingleThreadedNotification:這個通知目前沒有實際意義,可以忽略
NSThreadWillExitNotification執行緒退出之前傳送這個通知
複製程式碼

以上,是NSThread的基礎屬於與API釋義與使用

二. 加鎖與效能

1. NSLock
// 嘗試加鎖,成功返回YES ;失敗返回NO ,但不會阻塞執行緒的執行
- (BOOL)tryLock;
// 在指定的時間以前得到鎖。YES:在指定時間之前獲得了鎖;NO:在指定時間之前沒有獲得鎖。該執行緒將被阻塞,直到獲得了鎖,或者指定時間過期。
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 返回鎖指定的name,有set和get方法
@property (nullable, copy) NSString *name;
複製程式碼

下邊是一個搶票的demo,涉及到加鎖,同步,執行緒間通訊,延時,解鎖,標記,退出等 屬性定義

@property (nonatomic, assign) NSInteger ticketNumber;
@property (nonatomic, assign) NSInteger useTicket;
@property (nonatomic, strong) NSThread *ticketThreadOne;
@property (nonatomic, strong) NSThread *ticketThreadTwo;
@property (nonatomic, strong) NSThread *ticketThreadThree;
@property (nonatomic, strong) NSThread *ticketThreadFourth;
@property (nonatomic, strong) NSLock *ticketLock;
複製程式碼

初始化

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _ticketNumber = 10;
    _useTicket = 0;
    
    _ticketLock = [[NSLock alloc]init];
    
    _ticketThreadOne = [[NSThread alloc] initWithTarget:self selector:@selector(grabTicket)
                                                 object:nil];
    _ticketThreadOne.name = @"first thread";
    [_ticketThreadOne start];
    
    _ticketThreadTwo = [[NSThread alloc] initWithTarget:self selector:@selector(grabTicket)
                                                 object:nil];
    _ticketThreadTwo.name = @"second thread";
    [_ticketThreadTwo start];
    
    _ticketThreadThree = [[NSThread alloc] initWithTarget:self selector:@selector(grabTicket)
                                                 object:nil];
    _ticketThreadThree.name = @"third thread";
    [_ticketThreadThree start];
    
    _ticketThreadFourth = [[NSThread alloc] initWithTarget:self selector:@selector(stealTicket)
                                                   object:nil];
    _ticketThreadFourth.name = @"foruth thread";
    [_ticketThreadFourth start];
}
複製程式碼

例項方法

- (void)grabTicket {
    while (true) {
        NSThread *currentThread = [NSThread currentThread];
        [_ticketLock lock];
        [NSThread sleepForTimeInterval:0.5];
        NSInteger surplusTicket = _ticketNumber - _useTicket;
        
        if (surplusTicket > 0) {
            _useTicket ++;
            surplusTicket = _ticketNumber - _useTicket;
            NSLog(@"%@成功搶到一張票,剩餘票數:%tu",currentThread.name,surplusTicket);
            if ([currentThread.name isEqualToString:@"first thread"]) {
                [NSThread sleepForTimeInterval:0.5];
                [self performSelectorOnMainThread:@selector(EatABun:) withObject:@{@"threadName":@"first thread"} waitUntilDone:YES];
                [NSThread sleepForTimeInterval:0.5];
                NSLog(@"吃完饅頭繼續搶票");
            } else if ([currentThread.name isEqualToString:@"second thread"]) {
                [NSThread sleepForTimeInterval:0.5];
                NSLog(@"我是第二條執行緒,我搶到票之後全體休息3秒鐘");
                [NSThread sleepForTimeInterval:3];
            } else if ([currentThread.name isEqualToString:@"third thread"]) {
                [NSThread sleepForTimeInterval:0.5];
                NSLog(@"我是第三條執行緒,我要連搶兩張票");
                if (surplusTicket > 0) {
                    _useTicket ++;
                } else {
                    NSLog(@"只剩一張了,沒搶上兩張");
                }
            }
        } else {
            if ([currentThread isCancelled]) {
                [_ticketLock unlock];
                break;
            } else {
                NSLog(@"%@搶票失敗,剩餘票數:%tu,所有票都被搶完",[NSThread currentThread],surplusTicket);
                [_ticketThreadOne cancel];
                [_ticketThreadTwo cancel];
                [_ticketThreadThree cancel];
            }
        }
        [_ticketLock unlock];
    }
}

- (void)stealTicket {
    while (true) {
        if ([_ticketThreadFourth isCancelled]) {
            [_ticketLock unlock];
            break;
        }
        NSInteger surplusTicket = _ticketNumber - _useTicket;
        if (surplusTicket <= 0) {
            if ([_ticketLock tryLock]) {
                NSLog(@"我是第四條執行緒,我過來增加10張票");
                _useTicket = 10;
                [_ticketThreadFourth cancel];
            }
        }
    }
}

- (void)EatABun:(NSDictionary *)threadData {
    NSLog(@"我是第一條執行緒,搶完票我來吃了一個饅頭,%@",[threadData objectForKey:@"threadName"]);
}
複製程式碼

需要注意的是:因為執行緒定義的時候是全域性變數,所以執行緒結束的時候也不會釋放。手動呼叫- (void)cancel;方法後執行緒將進入死亡狀態,在這個狀態下再次呼叫start方法會造成崩潰。

2. NSConditionLock

這個鎖的基本釋義和使用已經在Pthread篇解釋過了,這裡就不重複解釋了,下邊就是演示一下NSConditionLock與NSTHread的配合使用:


@interface NSThreadNSConditionLockController ()

@property (nonatomic, assign) NSInteger useTicket;
@property (nonatomic, strong) NSThread *ticketThreadOne;
@property (nonatomic, strong) NSConditionLock *conditionLock;

@end

@implementation NSThreadNSConditionLockController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.title = @"NSThreadNSConditionLockController";
    
    _useTicket = 0;
    
    _conditionLock = [[NSConditionLock alloc]initWithCondition:0];
    
    _ticketThreadOne = [[NSThread alloc] initWithTarget:self selector:@selector(grabTicket)
                                                 object:nil];
    _ticketThreadOne.name = @"first thread";
    [_ticketThreadOne start];
    
}

- (void)grabTicket {
    NSThread *currentThread = [NSThread currentThread];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while(true)
        {
            [_conditionLock lockWhenCondition:0];
            // 增加票數
            _useTicket ++;
            NSLog(@"最新票數 = %tu",_useTicket);
            [_conditionLock unlockWithCondition:(_useTicket >= 10 ? 10 : 0)];
  // 開啟後執行一次
//            if (_useTicket >= 10) {
//                break;
//            }
            [NSThread sleepForTimeInterval:0.5];
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (true)
        {
            [_conditionLock lockWhenCondition:10];
            // 搶票
            _useTicket --;
            NSLog(@"%@成功搶到一張票,剩餘票數:%tu",currentThread.name,_useTicket);
            if ([currentThread.name isEqualToString:@"first thread"]) {
                [self performSelectorOnMainThread:@selector(EatABun:) withObject:@{@"threadName":@"first thread"} waitUntilDone:YES];
                NSLog(@"吃完饅頭繼續搶票");
            }
            [_conditionLock unlockWithCondition:(_useTicket<=0 ? 0 : 10)];
            [NSThread sleepForTimeInterval:0.5];
        }
    });
}

- (void)EatABun:(NSDictionary *)threadData {
    NSLog(@"我是第一條執行緒,搶完票我來吃了一個饅頭,%@",[threadData objectForKey:@"threadName"]);
}

複製程式碼

使用這個鎖需要注意線上程沒有獲得鎖的情況下,會產生阻塞情況

3. NSRecursiveLock

NSRecursiveLock類定義的鎖可以在同一執行緒多次獲得,而不會造成死鎖。一個遞迴鎖會跟蹤它被多少次成功獲得了。每次成功的獲得該鎖都必須平衡呼叫鎖住和解鎖的操作。只有所有的鎖住和解鎖操作都平衡的時候,鎖才真正被釋放給其他執行緒獲得。

正如它名字所言,這種型別的鎖通常被用在一個遞迴函式裡面來防止遞迴造成阻塞執行緒。你可以類似的在非遞迴的情況下使用他來呼叫函式,這些函式的語義要求它們使用鎖。以下是一個簡單遞迴函式,它在遞迴中獲取鎖。如果你不在該程式碼裡使用NSRecursiveLock物件,當函式被再次呼叫的時候執行緒將會出現死鎖。

NSRecursiveLock除了實現NSLocking協議的方法外,還提供了兩個方法,分別如下:

// 在給定的時間之前去嘗試請求一個鎖
- (BOOL)lockBeforeDate:(NSDate *)limit
// 嘗試去請求一個鎖,並會立即返回一個布林值,表示嘗試是否成功
- (BOOL)tryLock
複製程式碼

下邊是在網上找到的一個遞迴例子:

NSLock *lock = [[NSLock alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        static void (^RecursiveMethod)(int);
        
        RecursiveMethod = ^(int value) {
            
            [lock lock];
            if (value > 0) {
                
                NSLog(@"value = %d", value);
                sleep(2);
                RecursiveMethod(value - 1);
            }
            [lock unlock];
        };
        
        RecursiveMethod(5);
    });
複製程式碼

block裡邊會重複的呼叫自己,但是解鎖操作在呼叫之前,這樣就導致了死鎖,執行緒被阻塞住了。所以會報這個錯誤:

-[NSLock lock]: deadlock (<NSLock: 0x6180002c0ee0> '(null)')
Break on _NSLockError() to debug.
複製程式碼

這樣的情況我們就可以使用NSRecursiveLock,上邊例子稍微做一下修改:

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    [NSThread detachNewThreadWithBlock:^{
        static void (^RecursiveMethod)(int);
        
        RecursiveMethod = ^(int value) {
            
            [lock lock];
            if (value > 0) {
                
                NSLog(@"value = %d", value);
                sleep(1);
                RecursiveMethod(value - 1);
            }
            [lock unlock];
        };
        
        RecursiveMethod(5);
    }];
複製程式碼

這樣就會有完整的輸出了

value = 5
value = 4
value = 3
value = 2
value = 1
複製程式碼

注意:遞迴鎖不會被主動釋放,直到所有鎖平衡使用瞭解鎖操作,所以你必須仔細權衡是否使用遞迴鎖,因為它對效能有潛在的影響。長時間持有一個鎖將會導致其他執行緒阻塞直到遞迴完成。如果你可以重寫你的程式碼來消除遞迴或消除使用一個遞迴鎖,你可能會獲得更好的效能。

3. @synchronized

最後說一下@synchronized這個玩意,@synchronized 結構所表現出來的功能和互斥鎖差不多,都是為了防止同一時間不同物件執行同一段程式碼。 我在這裡找到一篇對@synchronized解釋的很好的文章,有興趣的可以自己去看一下,因為這次寫的是多執行緒調研,關於鎖這方面的知識就不做深入研究了。 需要了解的:

  • @synchronized 結構在工作時為傳入的物件分配了一個遞迴鎖。
  • 你呼叫 sychronized 的每個物件,Objective-C runtime 都會為其分配一個遞迴鎖並儲存在雜湊表中。
  • 如果在 sychronized 內部物件被釋放或被設為 nil 看起來都 OK。不過這沒在文件中說明,所以我不會再生產程式碼中依賴這條。
  • 不要向你的 sychronized block 傳入 nil!這將會從程式碼中移走執行緒安全。你可以通過在 objc_sync_nil 上加斷點來檢視是否發生了這樣的事情。

由於@synchronized使用較多,就不做demo解釋了



有志者、事竟成,破釜沉舟,百二秦關終屬楚;

苦心人、天不負,臥薪嚐膽,三千越甲可吞吳.

相關文章