書接上回, 上次談到iOS 多線程知識點總結(jié)之: 進程和線程, 接著就是 多線程實現(xiàn)方案里面的 NSThread 了.
NSThread 多線程創(chuàng)建方法
方法一: alloc init, 需要手動啟動線程
// 1. 創(chuàng)建線程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:nil];
// 2. 啟動線程
[thread start];
- 通過 NSThread 調(diào)用的方法是必須只傳遞一個參數(shù), 而且不一定要有返回值,在文檔中是這樣解釋的
selector
The selector for the message to send to target. This selector must take only one argument and must not have a return value.
調(diào)用方法實現(xiàn):
- (void)test:(NSString *)string {
NSLog(@"test - %@ - %@", [NSThread currentThread], string);
}
通過打印結(jié)果知此時已經(jīng)創(chuàng)建了一個子線程(number = 2)
test - <NSThread: 0x7fc56070cbe0>{number = 2, name = (null)} - (null)
方法二: 分離子線程, 會自動啟動線程
[NSThread detachNewThreadSelector:@selector(test:) toTarget:self withObject:@"分離子線程"];
打印結(jié)果:
test - <NSThread: 0x7ff482c12b30>{number = 2, name = (null)} - 分離子線程
方法三: 開啟一條后臺線程, 也會自動啟動線程
[self performSelectorInBackground:@selector(test:) withObject:@"后臺線程"];
打印結(jié)果:
test - <NSThread: 0x7f983960fc50>{number = 2, name = (null)} - 后臺線程
三種方法對比
方法一
- 優(yōu)點: 可以拿到線程對象, 并設(shè)置相關(guān)屬性
- 缺點: 代碼量相對多一點, 需要手動啟動線程
方法二和方法三
- 優(yōu)點: 創(chuàng)建線程簡單快捷
- 缺點: 無法拿到線程對象, 無法設(shè)置相關(guān)屬性
NSThread 常用屬性設(shè)置
NSThread 里有很多的方法和屬性, 常用的有下圖中的兩個:
當(dāng)通過NSThread創(chuàng)建了不止一條線程的時候,就能用到這些了.
name (線程名字)
例如我們創(chuàng)建三條子線程,并設(shè)置子線程的name 屬性
// 創(chuàng)建線程A
NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:@"子線程"];
threadA.name = @"子線程A";
[threadA start];
// 創(chuàng)建線程B
NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:@"子線程"];
threadB.name = @"子線程B";
[threadB start];
// 創(chuàng)建線程C
NSThread *threadC = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:@"子線程"];
threadC.name = @"子線程C";
[threadC start];
這樣在想知道是哪條線程的時候,只需要打印鮮明名字就可以了[NSThread currentThread].name
,方便查看, 打印結(jié)果如下:
2016-07-27 14:51:20.520 多線程[75816:852836] test - 子線程A
2016-07-27 14:51:20.520 多線程[75816:852837] test - 子線程B
2016-07-27 14:51:20.520 多線程[75816:852838] test - 子線程C
threadPriority(線程優(yōu)先級)
threadPriority 的取值范圍是 0.0 -- 1.0, 默認(rèn)是0.5. 數(shù)值越大, 優(yōu)先級越高 ,通過代碼來演示下
這里給三個子線程設(shè)置了不同的優(yōu)先級, 線程A < 線程C < 線程B
threadA.threadPriority = 0.1;
threadB.threadPriority = 1.0;
threadC.threadPriority = 0.5;
讓三個線程都執(zhí)行100次, 打印一下各個線程的運行次數(shù)和線程名字:
for (int i = 0; i < 100; ++i) {
NSLog(@"%d - %@", i + 1, [NSThread currentThread].name);
}
執(zhí)行結(jié)果如下:
同一時間, 三個線程的執(zhí)行次數(shù)有很大差別,這是因為 線程B 的優(yōu)先級最大,被執(zhí)行的概率也最大, 執(zhí)行次數(shù)自然也最多, 線程A 的優(yōu)先級最小, 被執(zhí)行的概率最小, 執(zhí)行的次數(shù)自然也最小.
NSThread 線程的生命周期
- 只有當(dāng)需要執(zhí)行的任務(wù)全部執(zhí)行完畢之后才會被釋放掉.
這個證明起來也很簡單, 自定義一個 Thread 類繼承字 NSThread , 里面重寫一下 dealloc 方法, 打印一下方法名即可. 用自定義 Thread 創(chuàng)建一個線程, 會發(fā)現(xiàn)任務(wù)指向完畢之后, dealloc 方法被調(diào)用.
線程的狀態(tài)
做了一張圖
控制線程狀態(tài)
- 啟動線程
- (void)start;
線程進入就緒狀態(tài), 當(dāng)線程執(zhí)行完畢,進入死亡狀態(tài)
- 阻塞(暫停)線程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
線程進入阻塞狀態(tài)
代碼演示:
// 阻塞線程
//[NSThread sleepForTimeInterval:3.0];
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];
上面兩種方法的執(zhí)行效果是相同的, 開始和結(jié)束的之間線程阻塞或者說休眠了3秒
2016-07-27 17:10:51.223 控制線程狀態(tài)[84187:952380] test - <NSThread: 0x7fae6249f250>{number = 2, name = (null)}
2016-07-27 17:10:54.231 控制線程狀態(tài)[84187:952380] ----end----
- 強制停止線程
+ (void)exit;
線程進入死亡狀態(tài)
代碼演示:
讓任務(wù)執(zhí)行100次, 看下效果
- (void)test {
for (int i = 0; i < 100; ++i) {
NSLog(@"%d - %@", i, [NSThread currentThread]);
}
NSLog(@"----end----");
}
執(zhí)行完畢之后, 自動結(jié)束
讓任務(wù)在執(zhí)行過程中強制停止
- (void)test {
for (int i = 0; i < 100; ++i) {
NSLog(@"%d - %@", i, [NSThread currentThread]);
if (i == 10) {
[NSThread exit];
}
}
}
當(dāng)達到停止條件時, 線程就強制退出了
線程一旦進入到死亡狀態(tài), 線程也就停止了, 就不能再次啟動任務(wù).
線程安全
多線程的安全隱患
- 資源共享
- 一塊資源可能會被多個線程共享, 也就是多個線程可能會訪問同一塊資源
- 比如多個線程訪問同一個對象, 同一個變量, 同一個文件
- 當(dāng)多個線程訪問同一塊資源時, 很容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全問題
買火車票的例子
舉這個例子, 是為了模仿我們實際 iOS 開發(fā)中可能會用到多線程下載網(wǎng)絡(luò)數(shù)據(jù)的情況, 因為數(shù)據(jù)量可能會很大, 看是否會出現(xiàn)問題.
// 火車票總數(shù)
self.ticketCount = 100;
// 三個售票員
self.threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
self.threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
self.threadC = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
self.threadA.name = @"售票員A";
self.threadB.name = @"售票員B";
self.threadC.name = @"售票員C";
[self.threadA start];
[self.threadB start];
[self.threadC start];
買票方法:
- (void)saleTicket {
while (1) {
NSInteger count = self.ticketCount;
if (count > 0) {
for (int i = 0; i < 1000000; ++i) {
// 只是耗時間, 沒有其他用
}
self.ticketCount = count - 1;
NSLog(@"%@賣出一張票,還剩- %zd", [NSThread currentThread].name, self.ticketCount);
} else {
NSLog(@"票賣完了");
break;
}
}
}
因為簡單的買票操作執(zhí)行非匙Γ快,無法看出效果,就在其中加了一段耗費時間的代碼,這時候看到的結(jié)果是這樣的
顯然, 是有問題的, 多次出現(xiàn)賣出同一張票的情況. 也就是造成了數(shù)據(jù)混亂. 那該怎么解決呢? 這個時候就要用到 -- 互斥鎖.
- (void)saleTicket {
while (1) {
// 加 互斥鎖, 全局唯一, self, 代表鎖對象
@synchronized(self) {
NSInteger count = self.ticketCount;
if (count > 0) {
for (int i = 0; i < 1000000; ++i) {
// 只是耗時間, 沒有其他用
}
self.ticketCount = count - 1;
NSLog(@"%@賣出一張票,還剩- %zd", [NSThread currentThread].name, self.ticketCount);
} else {
NSLog(@"票賣完了");
break;
}
}
}
}
這樣就可以了,運行看結(jié)果:
不會出現(xiàn)數(shù)據(jù)混亂的情況了, 也達到了三個線程賣票的功能.
加鎖的注意點
- 必須是全局唯一的.
- 加鎖的位置
- 加鎖的前提條件(多條線程搶奪同一塊資源)
加鎖的優(yōu)點
- 能有效的防止因為多線程搶奪資源造成的數(shù)據(jù)安全問題
加鎖的缺點
- 會耗費一些額外的 CPU 資源
- 造成線程同步(多條線程在同一條線上執(zhí)行,而且是按順序的執(zhí)行)
原子和非原子 屬性
OC 在定義屬性時有 nonatomic 和atomic
- atomic: 原子性, 為 setter 方法加鎖(默認(rèn)是 atomic)
- nonatomic: 非原子性, 不會為 setter 方法加鎖
nonatomic 和**atomic **對比
- atomic: 線程安全, 需要消耗大量的資源
- nonatomic: 非線程安全, 適合內(nèi)存小的移動設(shè)備
iOS 開發(fā)建議
- 所有屬性都聲明為 nonatomic
- 盡量避免多線程搶奪同一塊資源
- 盡量將加鎖, 資源搶奪的業(yè)務(wù)邏輯交給服務(wù)器端處理, 減小移動端的壓力.
線程間通信
什么叫線程間通信
在一個進程中, 線程往往不是孤立存在的, 多個線程之間需要經(jīng)常的進行通信
線程間通信的體現(xiàn)
- 一個線程傳遞數(shù)據(jù)給另一個線程
- 在一個線程中執(zhí)行完畢特定任務(wù)后, 轉(zhuǎn)到另一個線程繼續(xù)執(zhí)行任務(wù)
線程鍵通信常用的方法
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
例如, 給一個在 view 上的 UIImageView 添加網(wǎng)絡(luò)圖片的操作
一般情況下,我們是直接給 imageView 設(shè)置圖片
// 網(wǎng)絡(luò)圖片 URL
NSURL *url = [NSURL URLWithString:@"http://pic1.win4000.com/wallpaper/2/4fcec0bf0fb7f.jpg"];
// 根據(jù) URL 下載圖片到本地, 保存為二進制文件
NSData *data = [NSData dataWithContentsOfURL:url];
// 轉(zhuǎn)換圖片格式
UIImage *image = [UIImage imageWithData:data];
// 設(shè)置圖片
self.imageView.image = image;
但是如果圖片比較大, 下載所需要的事件比較長, 這個時候就會造成主線程的阻塞, 影響用戶體驗.我們可以開啟一個子線程去加載圖片, 下載完畢之后再回到主線程顯示圖片, 這個就是線程之間的通信.
[NSThread detachNewThreadSelector:@selector(download) toTarget:self withObject:nil];
下載方法的實現(xiàn):
- (void)download {
// 網(wǎng)絡(luò)圖片 URL
NSURL *url = [NSURL URLWithString:@"http://pic1.win4000.com/wallpaper/2/4fcec0bf0fb7f.jpg"];
// 根據(jù) URL 下載圖片到本地, 保存為二進制文件
NSData *data = [NSData dataWithContentsOfURL:url];
// 轉(zhuǎn)換圖片格式
UIImage *image = [UIImage imageWithData:data];
// 查看當(dāng)前線程
NSLog(@"download - %@", [NSThread currentThread]);
// 在子線程下載后要回到主線程設(shè)置 UI
/*
第一個參數(shù): 回到主線程之后要調(diào)用哪個方法
第二個參數(shù): 調(diào)用方法要傳遞的參數(shù)
第三個參數(shù): 是否需要等待該方法執(zhí)行完畢再往下執(zhí)行
*/
[self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
}
設(shè)置并顯示圖片方法實現(xiàn):
- (void)showImage:(UIImage *)image {
// 設(shè)置圖片
self.imageView.image = image;
// 查看當(dāng)前線程
NSLog(@"showImage - %@", [NSThread currentThread]);
}
控制臺打印結(jié)果是:
可以看到,下載圖片是在子線程, 設(shè)置圖片是回到了主線程操作的
關(guān)于回到主線程設(shè)置圖片, 除了上面提到的方法,還是使用
[self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
這個也是需要調(diào)用 showImage
方法,效果一樣.
也可以直接使用self.imageView
調(diào)用performSelectorOnMainThread: withObject: waitUntilDone:
方法, 這樣不需要再去生命一個showImage
方法, 就可以回到主線程設(shè)置圖片.
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
也能達到我們想要的效果.
這也是線程間通信最常用的情景.
- 關(guān)于 NSThread 多線程的總結(jié)就到這里, 下篇將對 GCD 進行總結(jié)學(xué)習(xí)
相關(guān)文章:
iOS 多線程知識點總結(jié)之 -- 進程和線程
iOS多線程實現(xiàn)方案之--GCD