?? 當(dāng)新打開一個APP的時候,系統(tǒng)會新創(chuàng)建一個進(jìn)程。這個進(jìn)程會默認(rèn)新創(chuàng)建一個線程,把這個線程命名為主線程。多線程主要應(yīng)用于與服務(wù)器進(jìn)行數(shù)據(jù)傳輸?shù)纫恍┖臅r操作。為了防止阻塞主線程,影響用戶交互,我們必須要新建子線程來執(zhí)行一些耗時操作。本文主要通過介紹NSThread的使用方法,來探討線程的生命周期、線程安全,線程間通信。
1.線程的生命周期
??線程的生命周期分為:1.創(chuàng)建線程;2.調(diào)度任務(wù);3.銷毀線程。一個NSThread對象就是一條線程,獲得一個NSThread對象的方式有兩種。
- (void)viewDidLoad {
[super viewDidLoad];
// 方式1
NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
//調(diào)度任務(wù),例如下載圖片,往服務(wù)器上傳文件等一切耗費時間的操作
NSLog(@"thread1--------執(zhí)行任務(wù)");
}];
// 開始執(zhí)行線程中的任務(wù),相當(dāng)于調(diào)度任務(wù)
[thread1 start];
//方式 2
//argument:id類型,為向方法(executeTask:)中傳遞的參數(shù)
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(executeTask:) object:@{@"key" : @"value"}];
[thread2 start];
}
- (void)executeTask:(id)argument
{
/*
thread2--------執(zhí)行任務(wù)-------argument = {
key = value;
}
**/
NSLog(@"thread2--------執(zhí)行任務(wù)-------argument = %@",argument);
}
??創(chuàng)建一個NSThread對象并往線程中添加了任務(wù)之后,必須執(zhí)行[thread start];
才會執(zhí)行線程中的任務(wù)。[thread start];
只能執(zhí)行一次,否則會報attempt to start the thread again
的錯誤。
??當(dāng)線程中的任務(wù)執(zhí)行完之后,系統(tǒng)會自動執(zhí)行[NSThread exit]
銷毀線程,釋放內(nèi)存。在銷毀線程之前,[NSThread exit]
這個方法會發(fā)送一個通知NSThreadWillExitNotification
通知觀察者線程即將銷毀。由于通知的發(fā)出是同步的,所以回調(diào)的執(zhí)行在線程銷毀之前。所以我們可以用下面的方法來監(jiān)測線程的銷毀。
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
//調(diào)度任務(wù),例如下載圖片,往服務(wù)器上傳文件等一切耗費時間的操作
NSLog(@"thread1--------執(zhí)行任務(wù)");
}];
// 開始執(zhí)行線程中的任務(wù),相當(dāng)于任務(wù)調(diào)度
thread1.name = @"thread1";
[thread1 start];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(threadWillExit:) name:NSThreadWillExitNotification object:nil];
}
- (void)threadWillExit:(NSNotification *)noti
{
// 日志: thread1 馬上退出了
NSLog(@"%@ 馬上退出了",[[NSThread currentThread] name]);
}
??NSThread提供了很多屬性和方法,方便我們使用。下面主要介紹常用的幾個屬性和方法。
// 類屬性,獲取當(dāng)前線程 [NSThread currentThread]
@property (class, readonly, strong) NSThread *currentThread;
// 類屬性,獲取主線程 [NSThread mainThread]
@property (class, readonly, strong) NSThread *mainThread;
//線程的名字
@property (nullable, copy) NSString *name;
2.線程安全
??一塊數(shù)據(jù)如果被多個線程同時訪問,就容易發(fā)生數(shù)據(jù)錯亂和數(shù)據(jù)安全問題。下面舉一個賣票的例子。
- (void)viewDidLoad {
[super viewDidLoad];
// 一共50張票
self.ticketNum = 50;
// 三個售票員同時開始賣票
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
thread1.name = @"售票員A";
self.thread1 = thread1;
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
thread2.name = @"售票員B";
self.thread2 = thread2;
NSThread *thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
thread3.name = @"售票員C";
self.thread3 = thread3;
[thread1 start];
[thread2 start];
[thread3 start];
}
- (void)sellTickets
{
while (1) {
int count = self.ticketNum;
//1.先檢查票數(shù)
if (count > 0) {
//暫停一段時間
[NSThread sleepForTimeInterval:0.02];
//2.票數(shù)-1
self.ticketNum = count-1;
//獲取當(dāng)前線程
NSThread *current= [NSThread currentThread];
NSLog(@"%@--賣了一張票,還剩余%d張票",current,self.ticketNum);
}
if (self.ticketNum == 0){
//退出線程
[NSThread exit];
}
}
}
??因為日志太長,僅貼出部分日志。
2019-03-27 15:43:03.848178+0800 TestAppIOS[399:12301] <NSThread: 0x28354d0c0>{number = 4, name = 售票員B}--賣了一張票,還剩余49張票
2019-03-27 15:43:03.848178+0800 TestAppIOS[399:12302] <NSThread: 0x28354cf40>{number = 5, name = 售票員C}--賣了一張票,還剩余49張票
2019-03-27 15:43:03.848194+0800 TestAppIOS[399:12300] <NSThread: 0x28354d200>{number = 3, name = 售票員A}--賣了一張票,還剩余49張票
2019-03-27 15:43:03.868778+0800 TestAppIOS[399:12301] <NSThread: 0x28354d0c0>{number = 4, name = 售票員B}--賣了一張票,還剩余48張票
2019-03-27 15:43:03.872740+0800 TestAppIOS[399:12302] <NSThread: 0x28354cf40>{number = 5, name = 售票員C}--賣了一張票,還剩余48張票
2019-03-27 15:43:03.872797+0800 TestAppIOS[399:12300] <NSThread: 0x28354d200>{number = 3, name = 售票員A}--賣了一張票,還剩余48張票
??通過日志打印我們可以看出,一共50張票,但是ABC每個售票員都賣了50張票,這明顯是不對的。那問題出在哪里呢?三個線程同時訪問門票的數(shù)量(共享數(shù)據(jù)),發(fā)生了數(shù)據(jù)錯亂。
1.售票員A查詢門票數(shù)量的時候,發(fā)現(xiàn)門票還有50張,賣了一張,還剩49張。
2.售票員B查詢門票數(shù)量的時候(此時售票員A還沒有把門票賣出去),發(fā)現(xiàn)門票還有50張,賣了一張,還剩49張。
3.售票員C查詢門票數(shù)量的時候(此時售票員A.B還沒有把門票賣出去),發(fā)現(xiàn)門票還有50張,賣了一張,還剩49張。
4.這就出現(xiàn)了一個怪現(xiàn)象,每個售票員都賣出1張票,卻還剩49張門票的原因。
??那怎樣解決這個問題呢?一個售票員賣票的時候,其他兩個人等著。等這個售票員賣完了,這倆售票員其中的一個再賣。
- (void)sellTickets
{
while (1) {
// 加一把鎖
@synchronized (self) {
int count = self.ticketNum;
//1.先檢查票數(shù)
if (count > 0) {
//暫停一段時間
[NSThread sleepForTimeInterval:0.02];
//2.票數(shù)-1
self.ticketNum = count-1;
//獲取當(dāng)前線程
NSThread *current= [NSThread currentThread];
NSLog(@"%@--賣了一張票,還剩余%d張票",current,self.ticketNum);
}
}
if (self.ticketNum == 0){
//退出線程
[NSThread exit];
}
}
}
??除了@synchronized
,還有NSLock
也可以達(dá)到相同的效果,解決多線程資源共享產(chǎn)生的數(shù)據(jù)安全問題。@synchronized
就是對括號里的代碼加鎖,一個線程執(zhí)行完了之后,另一個線程才能執(zhí)行。(售票員A賣票呢,此時BC不能賣。等A賣完了,BC才能賣。BC再去查詢票的數(shù)量的時候就變成了49張(因為A賣了一張))。
??鎖完美解決了多線程帶來的數(shù)據(jù)安全問題,不過鎖需要消耗大量的CPU資源,所以我們開發(fā)的時候盡量避免這種場景。
3.線程間通信
??理論上講線程都不是孤立存在的,需要相互傳遞消息。最常見的就是我們把一些耗時操作放在子線程,例如下載圖片,但是下載完畢我們不能在子線程更新UI,因為只有主線程才可以更新UI和處理用戶的觸摸事件,否則程序會崩潰。此時,我們就需要把子線程下載完畢的數(shù)據(jù)傳遞到主線程,讓主線程更新UI,這就是線程間的通信。
原理
代碼
// 點擊屏幕開始下載圖片
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"當(dāng)前線程1=%@",[NSThread currentThread]);
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"當(dāng)前線程2=%@",[NSThread currentThread]);
NSString *strURL = @"http://pic33.nipic.com/20130916/3420027_192919547000_2.jpg";
UIImage *image = [self downloadImageWithURL:strURL];
if (image) {
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
}
}];
[thread start];
}
日志
2016-11-04 13:47:04.532 TTTTTTTTTT[10584:122182] 當(dāng)前線程1=<NSThread: 0x600000260c80>{number = 1, name = main}
2016-11-04 13:47:04.533 TTTTTTTTTT[10584:122269] 當(dāng)前線程2=<NSThread: 0x600000265d80>{number = 3, name = (null)}
??以上就是關(guān)于NSThread的大部分知識了。在開發(fā)中,我們真正應(yīng)用NSThread的時候并不多,因為NSThread需要我們自己創(chuàng)建線程,調(diào)度任務(wù)。而線程并不是創(chuàng)建的越多越好,雖然多線程可以提高CPU的利用效率,但是創(chuàng)建多了,反而會拉低CPU運行速度,因為線程本身也需要消耗內(nèi)存。所以創(chuàng)建幾個線程,得根據(jù)CPU的當(dāng)時狀況來判斷。這對iOS程序員來說是解決不了的問題,因為我們無法知道CPU的運行狀況。所以NSThread雖然簡單,但是有很多不確定的情況。我們經(jīng)常使用的還是GCD和NSOperation,至于原因,后文我會詳細(xì)講解。