iOS 多執行緒(二)NSThread
iOS 使用NSThread來代表執行緒,建立新執行緒也就是建立一個NSThread物件。
1 建立和啟動執行緒
在iOS10之前提供了兩種方法開啟執行緒。
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
第一種方法是例項方法,返回一個NSThread物件,必須呼叫start第二種方法是類方法,不會返回NSThread物件,直接建立並啟動執行緒。
iOS10增加了兩種建立啟動執行緒的方法
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
這兩個方法與上述類似,也是一個例項方法,一個類方法。只是執行方法體不一樣了。具體使用如下:
#import "ZPYViewController.h" @interface ZPYViewController () @end @implementation ZPYViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. [self makeThread1beforeIOS10]; [self makeThread2beforeIOS10]; [self makeThread1FromIOS10]; [self makeThread2FromIOS10]; } -(void)run{ for(int i=0;i<10;i++){ NSLog(@"%@,----i=%d",[NSThread currentThread],i); NSLog(@"isMainThread=%d",[NSThread isMainThread]); } } //iOS 10 之前NSThread兩種方法建立執行緒。如下: -(void) makeThread1beforeIOS10{ //第一種建立方式 例項方法 最多可以傳一個引數 NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [thread1 setName:@"fisrt blood"]; //啟動執行緒 [thread1 start]; BOOL isMain = [NSThread isMainThread]; NSLog(@"isMain=%d,thread1=%d",isMain,[thread1 isMainThread]); } //第二種建立方式 類方法。 最多可以傳一個引數 -(void) makeThread2beforeIOS10{ //類方法,不會返回NSThread物件,直接啟動執行緒 [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil]; } //iOS 10又添加了兩個方法建立執行緒。 // 需要iOS 10 才可以執行下面兩個方法。否則會出錯。 //第三種建立方式 例項方法 -(void) makeThread1FromIOS10{ 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) makeThread2FromIOS10{ //類方法,不會返回NSThread物件,直接啟動執行緒 [NSThread detachNewThreadWithBlock:^{ for(int i=0;i<10;i++){ NSLog(@"%@,第四種 i=%d",[NSThread currentThread],i); } }]; } @end
2 常用方法
主執行緒相關:
+ (NSThread *)mainThread; // 獲得主執行緒
- (BOOL)isMainThread; // 是否為主執行緒
+ (BOOL)isMainThread; // 是否為主執行緒
其他方法:
獲得當前執行緒
NSThread *current = [NSThreadcurrentThread];
執行緒的排程優先順序
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
- (double)threadPriority;
- (BOOL)setThreadPriority:(double)p;
排程優先順序的取值範圍是0.0 ~ 1.0,預設0.5,值越大,優先順序越高
自己開發時,建議一般不要修改優先順序
執行緒的名字
- (void)setName:(NSString *)n;
- (NSString *)name;
3 執行緒的狀態
NSThread *thread1 = [[NSThreadalloc]initWithTarget:selfselector:@selector(run)object:nil];
//啟動執行緒
[thread1 start];
五種狀態:新建、就緒、執行、阻塞、死亡。
新建:當程式建立一個執行緒後,該執行緒就處於新建狀態,此時它和其他OC物件一樣,僅僅有系統分配了記憶體,初始化了成員變數。
就緒:當執行緒物件呼叫start方法後,該執行緒處於就緒狀態。處於該狀態的執行緒並沒有開始執行,至於何時執行,取決於系統的排程。
執行:當系統排程到該執行緒時,進入執行狀態。呼叫到其他執行緒則該執行緒就又重新回到就緒狀態。
阻塞:當執行緒呼叫了sleep方法或等待同步鎖時進入阻塞狀態。
死亡:當執行緒執行完畢,或異常/強制退出,則該執行緒進入死亡狀態。
啟動執行緒
- (void)start; //進入就緒狀態-> 執行狀態。當執行緒任務執行完畢,自動進入死亡狀態
阻塞(暫停)執行緒
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 進入阻塞狀態
強制停止執行緒
+ (void)exit;
// 進入死亡狀態
注意:一旦執行緒停止(死亡)了,就不能再次開啟任務
4 停止執行緒
有時我們需要將子執行緒結束,這該怎麼辦呢?
執行緒結束方式又三種:
1 執行緒執行體方法執行完成,執行緒正常結束。
2 執行緒執行過程中發生了異常。
3 呼叫NSThread的exit方法類停止當前正在執行的執行緒。
由於exit方法為類方法,所以只能停止正在執行的執行緒。
我們可以在主執行緒或者其他執行緒中呼叫[thread cancel]。而子執行緒的執行體中可以判斷是否cancel來停止執行緒。
//結束執行緒
if([[NSThread currentThread] isCancelled]){
[NSThread exit];
}
5 多執行緒安全
在多執行緒中,必然會涉及到執行緒安全點問題,比如資源共享(多個執行緒訪問同一個物件、同一個變數、同一個檔案),這時很容易發生資料錯亂、資料安全問題。
如下經典問題,取錢
如上圖:1500取出1600,還剩餘700,這樣顯然是不合理的(雖然對於使用者來說很樂意了)。
-(void)takeMoney:(NSNumber *)count{
[_account take:count.doubleValue];
}
- (IBAction)actionTest:(id)sender {
_account = [[ZPYAccount alloc] initWithCardId:@"1234567" andBalance:1500];
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(takeMoney:) object:[NSNumber numberWithInt:800]];
[thread1 setName:@"first thread"];
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(takeMoney:) object:[NSNumber numberWithInt:800]];
[thread2 setName:@"second thread"];
[thread1 start];
[thread2 start];
}
兩個執行緒去做取錢的操作。
ZPYAccount中的take方法:
-(void)take:(double)count{
double money = _balance;
if(count <= money){
[NSThread sleepForTimeInterval:0.001];
_balance = money - count;
NSLog(@"thread:%@,取錢成功,餘額為%f",[[NSThread currentThread] name],_balance);
}else{
NSLog(@"thread:%@,餘額不足!",[[NSThread currentThread] name]);
}
}
輸出如下:2016-10-11 11:40:09.052 NSThread使用[553:386755] thread:second thread,取錢成功,餘額為700.000000
2016-10-11 11:40:09.051 NSThread使用[553:386754] thread:first thread,取錢成功,餘額為700.000000
1 @synchronized實現同步:
-(void)take:(double)count{
@synchronized (self) {
//將需要同時執行的放入同步程式碼塊中。查錢、取錢。
double money = _balance;
if(count <= money){
[NSThread sleepForTimeInterval:0.001];
_balance = money - count;
NSLog(@"thread:%@,取錢成功,餘額為%f",[[NSThread currentThread] name],_balance);
}else{
NSLog(@"thread:%@,餘額不足!",[[NSThread currentThread] name]);
}
}
}
列印:2016-10-11 18:45:52.135 NSThread使用[556:387839] thread:first thread,取錢成功,餘額為700.000000
2016-10-11 18:45:52.137 NSThread使用[556:387840] thread:second thread,餘額不足!
這樣就合理了,當餘額不足時就不再讓取錢,並且餘額也沒有問題。
2 NSLock 同步鎖
使用NSLock也可以保證執行緒安全。
_lock = [[NSLockalloc]init];
-(void)takelock:(double)count{
[_lock lock];
double money = _balance;
if(count <= money){
[NSThread sleepForTimeInterval:0.001];
_balance = money - count;
NSLog(@"thread:%@,取錢成功,餘額為%f",[[NSThread currentThread] name],_balance);
}else{
NSLog(@"thread:%@,餘額不足!",[[NSThread currentThread] name]);
}
[_lock unlock];
}
列印:
2016-10-11 18:52:09.216 NSThread使用[559:388860] thread:first thread,取錢成功,餘額為700.000000
2016-10-11 18:52:09.217 NSThread使用[559:388861] thread:second thread,餘額不足!