線程概述
有些程序是一條直線,起點(diǎn)到終點(diǎn);有些程序是一個(gè)圓,不斷循環(huán),直到將它切斷
一個(gè)運(yùn)行著的程序就是一個(gè)進(jìn)程或者叫做一個(gè)任務(wù),一個(gè)進(jìn)程至少包含一個(gè)線程,線程就是程序的執(zhí)行流。Mac和iOS中的程序啟動(dòng),創(chuàng)建好一個(gè)進(jìn)程的同時(shí), 一個(gè)線程便開始運(yùn)行,這個(gè)線程叫主線程。主線程在程序中的地位和其他線程不同,它是其他線程最終的父線程,且所有界面的顯示操作即AppKit或 UIKit的操作必須在主線程進(jìn)行。
系統(tǒng)中的每一個(gè)進(jìn)程都有自己獨(dú)立的虛擬內(nèi)存空間,而同一個(gè)進(jìn)程中的多個(gè)線程則共用進(jìn)程的內(nèi)存空間。每創(chuàng)建一個(gè)新的線程,都需要一些內(nèi)存(如每個(gè)線程有自己的Stack空間)和消耗一定的CPU時(shí)間。另外當(dāng)多個(gè)線程對(duì)同一個(gè)資源出現(xiàn)爭(zhēng)奪的時(shí)候需要注意線程安全問題
多線程的實(shí)現(xiàn)原理:雖然在同一時(shí)刻,CPU只能處理1條線程,但是CPU可以快速地在多條線程之間調(diào)度(切換),造成了多線程并發(fā)執(zhí)行的假象。
多線程的優(yōu)點(diǎn)
能適當(dāng)提高程序的執(zhí)行效率。
能適當(dāng)提高資源利用率(CPU、內(nèi)存利用率)。
多線程的缺點(diǎn)
創(chuàng)建線程是需要成本的:iOS下主要成本包括:在??臻g的子線程512KB、主線程1MB,創(chuàng)建線程大約需要90毫秒的創(chuàng)建時(shí)間。
線程越多,CPU在調(diào)度線程上的開銷就越大。
線程越多,程序設(shè)計(jì)就越復(fù)雜:因?yàn)橐紤]到線程之間的通信,多線程的數(shù)據(jù)共享。
下面開始擼代碼:
------------------------------------------------------------------華麗的分割線
1.耗時(shí)操作的問題演示
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self longOperation];
}
- (void)longOperation
{
NSLog(@"start");
NSTimeInterval start = CACurrentMediaTime();
for (int i = 0; i < 10000000; i++) {
// 存儲(chǔ)在棧區(qū)
// int num = 10;
// 存儲(chǔ)在常量區(qū)
// NSString *str1 = @"hello";
// 存儲(chǔ)在堆區(qū)
// NSString *str2 = [NSString stringWithFormat:@"hello_%d",i];
// I/O操作 : 把數(shù)據(jù)從內(nèi)存輸出到外接設(shè)備,或者由外接設(shè)備輸入到內(nèi)存;
NSLog(@"%d",i);
}
NSLog(@"over %f", CACurrentMediaTime() - start);
}
結(jié)論
- 空的for循環(huán)不耗時(shí)
- 操作內(nèi)存的棧區(qū)速度很快;棧區(qū)存儲(chǔ)空間地址是連續(xù)的;
- 操作內(nèi)存的常量區(qū)速度很快;內(nèi)存空間只開辟一次;
- 操作內(nèi)存的堆區(qū)速度相對(duì)棧區(qū)和常量區(qū)要慢些;堆區(qū)內(nèi)存空間不連續(xù),需要尋址;
- I/O操作是很耗時(shí)的; (把數(shù)據(jù)從內(nèi)存輸出到外接設(shè)備,或者由外接設(shè)備輸入到內(nèi)存)
- 耗時(shí)操作對(duì)UI交互的影響 : 卡死了主屏幕,直到耗時(shí)操作執(zhí)行完,屏幕的交互才能正常進(jìn)行;
- 解決耗時(shí)操作卡頓UI的辦法 : 多線程技術(shù);
- 學(xué)習(xí)多線程的目的 : 把耗時(shí)操作放在后臺(tái)執(zhí)行,不讓耗時(shí)操作卡頓UI;
2.解決耗時(shí)操作卡頓UI的辦法
使用多線程技術(shù) : 解決屏幕卡死的問題
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [self longOperation];
// 使用多線程技術(shù)
[self performSelectorInBackground:@selector(longOperation) withObject:nil];
}
3.多線程基本概念
同步 & 異步
同步和異步是任務(wù)執(zhí)行的兩種方式-
同步
- 我們之前寫程序的時(shí)候代碼都是從上往下,順序執(zhí)行的,就叫做
同步執(zhí)行. - 1個(gè)人執(zhí)行多個(gè)任務(wù),是要依次執(zhí)行的.因?yàn)?個(gè)人同一時(shí)間只能執(zhí)行1個(gè)任務(wù).
-
多個(gè)任務(wù)按序依次執(zhí)行,就是同步執(zhí)行.
- 我們之前寫程序的時(shí)候代碼都是從上往下,順序執(zhí)行的,就叫做
-
異步
-
多個(gè)任務(wù)同時(shí)執(zhí)行,就是異步執(zhí)行. - 異步是多線程的代名詞.
- 我們學(xué)習(xí)多線程就是為了實(shí)現(xiàn)如何讓任務(wù)異步執(zhí)行.
-
進(jìn)程 & 線程
-
進(jìn)程
- 在系統(tǒng)中
正在運(yùn)行的一個(gè)應(yīng)用程序叫進(jìn)程. - 通過
活動(dòng)監(jiān)視器可以查看MAC系統(tǒng)中正在運(yùn)行的所有應(yīng)用程序. - 每個(gè)進(jìn)程之間都是
獨(dú)立的,均運(yùn)行在其專用且受保護(hù)的內(nèi)存空間內(nèi). - 兩個(gè)進(jìn)程之間是無法通信的,迅雷無法幫助酷我下載正在播放的音樂.
-
進(jìn)程可以類比成正在正常運(yùn)營的公司.
- 在系統(tǒng)中
-
線程
- 線程可以類比成公司中的
員工. - 進(jìn)程要想執(zhí)行任務(wù),必須要有線程,且每個(gè)進(jìn)程至少有一條線程.
- 線程是進(jìn)程的
基本執(zhí)行單元,進(jìn)程中的所有任務(wù)都在線程中執(zhí)行. - 程序啟動(dòng)(進(jìn)程開啟)會(huì)默認(rèn)開啟一條線程.
- 1個(gè)進(jìn)程中可以有多個(gè)線程.
- 線程可以類比成公司中的
多線程
-
多線程 : 一個(gè)進(jìn)程中可以開啟多條線程,多條線程可以
**同時(shí)**執(zhí)行不同的任務(wù). - 進(jìn)程-公司,線程-員工,老板是什么?
- 多線程可以解決程序阻塞的問題
- 多線程可以提高程序的執(zhí)行效率,給用戶良好的使用體驗(yàn).
- 比如,酷我音樂的邊下載邊聽歌,迅雷的邊下載邊播放.
4.多線程執(zhí)行原理
-
單核CPU同一時(shí)間,CPU只能處理1個(gè)線程,只有1個(gè)線程在執(zhí)行任務(wù). 多線程的同時(shí)執(zhí)行 : 其實(shí)是CPU在多條線程之間快速切換(調(diào)度任務(wù)).- 如果CPU調(diào)度線程的速度足夠快,就造成了多線程
**同時(shí)**執(zhí)行的**假象** - 如果線程非常多,CPU會(huì)在多條線程之間不斷的調(diào)度任務(wù),結(jié)果就是消耗了大量的CPU資源,CPU會(huì)累趴下.
- 每個(gè)線程調(diào)度的頻率會(huì)降低
- 線程的執(zhí)行效率會(huì)下降
5.多線程優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 能"適當(dāng)"提高程序的執(zhí)行效率.
- 能"適當(dāng)"提高CPU和內(nèi)存的利用率.
- 線程上的任務(wù)執(zhí)行完成后,線程會(huì)自動(dòng)銷毀,節(jié)省內(nèi)存.
缺點(diǎn)
- 開啟線程需要占用一定的內(nèi)存空間,如果開啟的線程過多,會(huì)占用大量的CPU資源,降低程序的性能
- 占用內(nèi)存空間:默認(rèn)情況下,子線程512KB,主線程1M.PS:iOS8中,主線程512KB.
- 線程越多,CPU調(diào)度線程的開銷就越大.
- 時(shí)間開銷
- 空間開銷
- 程序設(shè)計(jì)更加復(fù)雜:比如線程之間的通信,多線程的數(shù)據(jù)共享
6.主線程
-
一個(gè)程序運(yùn)行后,默認(rèn)會(huì)開啟1個(gè)線程,稱為
主線程或UI線程.- 關(guān)注
main函數(shù)的執(zhí)行
- 關(guān)注
主線程一般用來
刷新UI界面,處理UI事件.處理UI事件:點(diǎn)擊、滾動(dòng)、拖拽等事件-
主線程使用注意
- 別將耗時(shí)的操作放到主線程中
- 耗時(shí)操作會(huì)卡住主線程,嚴(yán)重影響UI的流暢度,給用戶一種卡的壞體驗(yàn),影響UI交互質(zhì)量.
7.多線程的實(shí)現(xiàn)方案
(二)創(chuàng)建線程三種方式
1.準(zhǔn)備新線程執(zhí)行的方法
- (void)demo:(id)obj
{
NSLog(@"傳入?yún)?shù) => %@",obj);
NSLog(@"hello %@",[NSThread currentThread]);
}
2.對(duì)象方法創(chuàng)建
- 實(shí)例化線程對(duì)象的同時(shí)指定線程執(zhí)行的方法
@selector(demo:). - 需要
手動(dòng)開啟線程.
- (void)threadDemo1
{
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:@"alloc"];
// 手動(dòng)啟動(dòng)線程
[thread start];
}
3.類方法創(chuàng)建
- 分離出一個(gè)線程,并且
自動(dòng)開啟線程執(zhí)行@selector(demo:). - 無法獲取到線程對(duì)象
- (void)threadDemo2
{
[NSThread detachNewThreadSelector:@selector(demo:) toTarget:self withObject:@"detach"];
}
4.NSObject(NSThreadPerformAdditions) 的分類創(chuàng)建
- 方便任何繼承自
NSObject的對(duì)象,都可以很容易的調(diào)用線程方法 - 無法獲取到線程對(duì)象
-
自動(dòng)開啟線程執(zhí)行@selector(demo:).
- (void)threadDemo3
{
[self performSelectorInBackground:@selector(demo:) withObject:@"perform"];
}
5.總結(jié)
- 以上三種創(chuàng)建線程的方式,各有不同.隨意選擇.
- 使用哪種方式需要根據(jù)具體的需求而定.比如 : 如果需要線程對(duì)象,就使用對(duì)象方法創(chuàng)建.
(三)target和selector的關(guān)系
1.target和selector的關(guān)系分析
-
target: 指方法從屬于的對(duì)象.- 比如 : 本對(duì)象--
self;其他對(duì)象--self.person.
- 比如 : 本對(duì)象--
-
@selector: 指對(duì)象里面的方法.- 比如 : 要執(zhí)行的是
self中或者self.person中的哪個(gè)方法.
- 比如 : 要執(zhí)行的是
-
提示 : 不要看見
target就寫self. -
target和@selector的關(guān)系 : 執(zhí)行哪個(gè)對(duì)象上的哪個(gè)方法.
2.代碼演練
準(zhǔn)備Person對(duì)象
@interface Person : NSObject
/// 人名
@property (nonatomic,copy) NSString *name;
/// 創(chuàng)建人的構(gòu)造方法
+ (instancetype)personWithDict:(NSDictionary *)dict;
/// 人有個(gè)方法
- (void)personDemo:(id)obj;
@end
@implementation Person
+ (instancetype)personWithName:(NSString *)name
{
Person *person = [[Person alloc] init];
person.name = name;
return person;
}
- (void)personDemo:(id)obj
{
NSLog(@"創(chuàng)建的人名 => %@",self.name);
NSLog(@"hello %@",[NSThread currentThread]);
}
@end
控制器中的使用
定義屬性
@interface ViewController ()
@property (nonatomic,strong) Person *person;
@end
懶加載Person
@implementation ViewController
- (Person *)person
{
if (_person==nil) {
_person = [Person personWithName:@"zhangjie"];
}
return _person;
}
新的實(shí)例化方法
使用
self調(diào)用@selector(personDemo:)就會(huì)崩潰.因?yàn)?code>self中沒有@selector(personDemo:).分類方法
// 崩潰
[self performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
// 正確的調(diào)用方式
[self.person performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
- 類方法
// 崩潰
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self withObject:@"detach"];
// 正確的調(diào)用方式
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self.person withObject:@"detach"];
- 對(duì)象方法
// 崩潰
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(personDemo:) object:@"alloc"];
// 手動(dòng)開啟線程
[thread start];
// 正確的調(diào)用方式
NSThread *thread = [[NSThread alloc] initWithTarget:self.person selector:@selector(personDemo:) object:@"alloc"];
// 手動(dòng)開啟線程
[thread start];
(四)線程狀態(tài)-生命周期
線程生命周期的控制
- 新建
- 內(nèi)存中創(chuàng)建了一個(gè)線程對(duì)象
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
- 就緒
- 將線程放進(jìn)
可調(diào)度線程池,等待被CPU調(diào)度
- 將線程放進(jìn)
[thread start];
-
運(yùn)行
- CPU負(fù)責(zé)調(diào)度 可調(diào)度線程池 中的處于 就緒狀態(tài) 的線程
- 線程執(zhí)行結(jié)束之前,狀態(tài)可能會(huì)在 就緒狀態(tài) 和 運(yùn)行狀態(tài) 之間來回的切換
- 就緒狀態(tài) 和 運(yùn)行狀態(tài) 之間的狀態(tài)切換由CPU來完成,程序員無法干涉
-
阻塞
-
正在運(yùn)行的線程,當(dāng)滿足某個(gè)條件時(shí),可以用
休眠或者鎖來阻塞線程的執(zhí)行- sleepForTimeInterval:休眠指定時(shí)長
[NSThread sleepForTimeInterval:1.0];- sleepUntilDate:休眠到指定日期
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];- 互斥鎖
@synchronized(self)
-
-
死亡
- 正常死亡:線程執(zhí)行結(jié)束
- 非正常死亡
- 程序突然崩潰
- 當(dāng)滿足某個(gè)條件后,在線程內(nèi)部強(qiáng)制線程退出,調(diào)用
exit方法
代碼演練
創(chuàng)建線程對(duì)象和就緒狀態(tài)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 新建狀態(tài)
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
// 就緒狀態(tài) : 將線程放進(jìn)"可調(diào)度線程池",等待被CPU調(diào)度.
[thread start];
}
新線程執(zhí)行的方法
- (void)threadDemo
{
// 提示 : 能執(zhí)行到這里說明線程是運(yùn)行狀態(tài)
NSLog(@"%@",[NSThread currentThread]);
// 使當(dāng)前線程休眠2秒鐘 : 休眠指定時(shí)長
[NSThread sleepForTimeInterval:2.0];
NSLog(@"第一次睡醒");
// 使當(dāng)前線程休眠到指定日期
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
NSLog(@"第二次睡醒");
// 使當(dāng)前線程退出 : 當(dāng)前線程一旦退出,后續(xù)的所有代碼都不會(huì)執(zhí)行
// 注意 : 該方法不能在主線程使用,會(huì)使主線程退出
[NSThread exit];
NSLog(@"沒戲了?");
}
關(guān)于exit的結(jié)論
- 使
當(dāng)前線程退出. - 不能在主線程中調(diào)用該方法.會(huì)使主線程退出.
-
當(dāng)前線程死亡之后,這個(gè)線程中的剩下的所有代碼都不會(huì)被執(zhí)行. - 在調(diào)用此方法之前一定要注意釋放之前由C語言框架創(chuàng)建的對(duì)象.
- 調(diào)用
exit方法屬于在線程內(nèi)部取消線程,有時(shí)候需要在線程外部,當(dāng)某一條件滿足時(shí)就取消線程
線程的取消 (在線程執(zhí)行的方法的外部取消)
- 取消線程的方法
- (void)cancel NS_AVAILABLE(10_5, 2_0);
- 使用
[thread cancel]
- 這個(gè)方法只是修改了線程的狀態(tài)而已,并沒有真正的取消線程
- 如果想真正的取消線程需要在線程執(zhí)行的過程中判斷線程的狀態(tài)是否是已取消
- 如果該線程已經(jīng)被取消,就直接返回,不再執(zhí)行后面的代碼
if ([NSThread currentThread].isCancelled) {
NSLog(@"該線程已經(jīng)被取消");
return;
}
(五)線程屬性
1.常用屬性
-
name- 線程名稱- 設(shè)置線程名稱可以當(dāng)線程執(zhí)行的方法內(nèi)部出現(xiàn)異常時(shí),記錄異常和當(dāng)前線程
-
stackSize- 棧區(qū)大小- 默認(rèn)情況下,無論是主線程還是子線程,棧區(qū)大小都是
512K - 棧區(qū)大小可以設(shè)置
[NSThread currentThread].stackSize = 1024 * 1024; - 必須是 4KB 的倍數(shù)
- 默認(rèn)情況下,無論是主線程還是子線程,棧區(qū)大小都是
isMainThread- 是否主線程-
threadPriority- 線程優(yōu)先級(jí)- 優(yōu)先級(jí),是一個(gè)浮點(diǎn)數(shù),取值范圍從
0~1.0 -
1.0表示優(yōu)先級(jí)最高 -
0.0表示優(yōu)先級(jí)最低 - 默認(rèn)優(yōu)先級(jí)是
0.5 - 優(yōu)先級(jí)高只是保證
CPU調(diào)度的可能性會(huì)高
- 優(yōu)先級(jí),是一個(gè)浮點(diǎn)數(shù),取值范圍從
-
qualityOfService- 服務(wù)質(zhì)量(iOS 8.0 推出)-
NSQualityOfServiceUserInteractive- 用戶交互,例如繪圖或者處理用戶事件 -
NSQualityOfServiceUserInitiated- 用戶需要 -
NSQualityOfServiceUtility- 實(shí)用工具,用戶不需要立即得到結(jié)果 -
NSQualityOfServiceBackground- 后臺(tái) -
NSQualityOfServiceDefault- 默認(rèn),介于用戶需要和實(shí)用工具之間
-
關(guān)于優(yōu)先級(jí)和服務(wù)質(zhì)量
- 多線程的目的:是將耗時(shí)的操作放在后臺(tái),不阻塞主線程和用戶的交互!
- 多線程開發(fā)的原則:簡(jiǎn)單
- 在開發(fā)時(shí),最好不要修改優(yōu)先級(jí),不要相信 用戶交互 服務(wù)質(zhì)量
- 內(nèi)核調(diào)度算法在決定該運(yùn)行哪個(gè)線程時(shí),會(huì)把線程的優(yōu)先級(jí)作為考量因素
* 較高優(yōu)先級(jí)的線程會(huì)比較低優(yōu)先級(jí)的線程具有更多的運(yùn)行機(jī)會(huì)
* 較高優(yōu)先級(jí)不保證你的線程具體執(zhí)行的時(shí)間,只是相比較低優(yōu)先級(jí)的線程,更有可能被調(diào)度器選擇執(zhí)行而已
2.代碼演示
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"主線程棧區(qū)空間大小 == %tu KB 是否是主線程 %zd",[NSThread currentThread].stackSize / 1024,[NSThread currentThread].isMainThread);
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
// 給線程起名字
thread1.name = @"download A";
// 設(shè)置線程優(yōu)先級(jí)
thread1.threadPriority = 1.0;
// 線程就緒
[thread1 start];
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
thread2.name = @"download B";
thread2.threadPriority = 0;
[thread2 start];
}
- (void)demo
{
NSLog(@"子線程棧區(qū)空間大小 == %tu KB 是否是主線程 %zd",[NSThread currentThread].stackSize / 1024,[NSThread currentThread].isMainThread);
for (int i = 0; i < 10; i++) {
NSLog(@"%@",[NSThread currentThread]);
}
}
3.補(bǔ)充
-
NSInteger有符號(hào)整數(shù)(有正負(fù)數(shù))用%zd -
NSUInteger無符號(hào)整數(shù)(沒有負(fù)數(shù))用%tu - 是為了
自適應(yīng)32位和64位CPU的架構(gòu).
(六)線程安全-資源共享
1.多線程操作共享資源的問題
-
共享資源
- 資源 : 一個(gè)全局的對(duì)象、一個(gè)全局的變量、一個(gè)文件.
- 共享 : 可以被多個(gè)對(duì)象訪問.
- 共享資源 :可以被多個(gè)對(duì)象訪問的資源.比如全局的對(duì)象,變量,文件.
在
多線程的環(huán)境下,共享的資源可能會(huì)被多個(gè)線程共享,也就是多個(gè)線程可能會(huì)操作同一塊資源.當(dāng)多個(gè)線程操作同一塊資源時(shí),很容易引發(fā)數(shù)據(jù)錯(cuò)亂和數(shù)據(jù)安全問題,數(shù)據(jù)有可能丟失,有可能增加,有可能錯(cuò)亂.
經(jīng)典案例 : 賣票.
-
線程安全
- 同一塊資源,被多個(gè)線程同時(shí)讀寫操作時(shí),任然能夠得到正確的結(jié)果,稱之為線程是安全的.
開發(fā)提示
- 實(shí)際開發(fā)中確定開發(fā)思路邏輯比及時(shí)的寫代碼更重要.
- 多線程開發(fā)的復(fù)雜度相對(duì)較高,在開發(fā)時(shí)可以按照以下套路編寫代碼
- 首先確保單個(gè)線程執(zhí)行正確
- 然后再添加線程
代碼實(shí)現(xiàn)賣票邏輯
- 先定義共享資源
@interface ViewController ()
/// 總票數(shù)(共享的資源)
@property (nonatomic,assign) int tickets;
@end
- 初始化余票數(shù)
共享資源
- (void)viewDidLoad {
[super viewDidLoad];
// 設(shè)置余票數(shù)
self.tickets = 20;
}
- 賣票邏輯實(shí)現(xiàn)
- (void)saleTickets
{
// while 循環(huán)保證每個(gè)窗口都可以單獨(dú)把所有的票賣完
while (YES) {
// 判斷是否有票
if (self.tickets>0) {
// 模擬網(wǎng)絡(luò)延遲 : 放大出錯(cuò)時(shí)的效果,沒有實(shí)際意義
[NSThread sleepForTimeInterval:1.0];
// 有票就賣一張
self.tickets--;
// 賣完一張票就提示用戶余票數(shù)
NSLog(@"剩余票數(shù) => %zd %@",self.tickets,[NSThread currentThread]);
} else {
// 沒有就提示用戶
NSLog(@"沒票了");
// 此處要結(jié)束循環(huán),不然會(huì)死循環(huán)
break;
}
}
}
單線程
- 先確保單線程中運(yùn)行正常
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 在主線程中賣票
[self saleTickets];
}
多線程
- 如果單線程運(yùn)行正常,就修改代碼,實(shí)現(xiàn)多線程環(huán)境
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 在主線程中賣票
// [self saleTickets];
// 售票口 A
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
thread1.name = @"售票口 A";
[thread1 start];
// 售票口 B
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
thread2.name = @"售票口 B";
[thread2 start];
}
資源搶奪結(jié)果
-
數(shù)據(jù)錯(cuò)亂,數(shù)據(jù)增加.
出錯(cuò)原因分析
2.解決多線程操作共享資源的問題
- 解決辦法 : 使用
互斥鎖/同步鎖.
添加互斥鎖
- (void)saleTickets
{
// while 循環(huán)保證每個(gè)窗口都可以單獨(dú)把所有的票賣完
while (YES) {
// 添加互斥鎖
@synchronized(self) {
// 判斷是否有票
if (self.tickets>0) {
// 模擬網(wǎng)絡(luò)延遲 : 放大出錯(cuò)時(shí)的效果,沒有實(shí)際意義
[NSThread sleepForTimeInterval:1.0];
// 有票就賣一張
self.tickets--;
// 賣完一張票就提示用戶余票數(shù)
NSLog(@"剩余票數(shù) => %zd",self.tickets);
} else {
// 沒有就提示用戶
NSLog(@"沒票了");
// 此處要結(jié)束循環(huán),不然會(huì)死循環(huán)
break;
}
}
}
}
互斥鎖小結(jié)
- 互斥鎖,就是使用了線程同步技術(shù).
- 同步鎖/互斥鎖:可以保證被鎖定的代碼,同一時(shí)間,只能有一個(gè)線程可以操作.
-
self:鎖對(duì)象,任何繼承自NSObject的對(duì)像都可以是鎖對(duì)象,因?yàn)閮?nèi)部都有一把鎖,而且默認(rèn)是開著的. - 鎖對(duì)象 : 一定要是全局的鎖對(duì)象,要保證所有的線程都能夠訪問,
self是最方便使用的鎖對(duì)象. - 互斥鎖鎖定的范圍應(yīng)該盡量小,但是一定要鎖住資源的
讀寫部分. - 加鎖后程序執(zhí)行的效率比不加鎖的時(shí)候要低.因?yàn)榫€程要等待解鎖.
- 犧牲了性能保證了安全性.
(七)原子屬性
1.原子屬性相關(guān)概念
nonatomic: 非原子屬性-
atomic: 原子屬性- 線程安全的,針對(duì)多線程設(shè)計(jì)的屬性修飾符,是默認(rèn)值.
- 特點(diǎn) : 單寫多讀
- **單寫多讀 : **保證同一時(shí)間,只有一個(gè)線程能夠執(zhí)行
setter方法,但是可以有多個(gè)線程執(zhí)行getter方法. -
atomic屬性的setter里面里面有一把鎖,叫做自旋鎖. - 原子屬性的
setter方法是線程安全的;但是,getter方法不是線程安全的.
-
nonatomic和atomic對(duì)比-
nonatomic: 非線程安全,適合內(nèi)存小的移動(dòng)設(shè)備. -
atomic: 線程安全,需要消耗大量的資源.性能比非原子屬性要差一點(diǎn)兒點(diǎn)兒.
-
2.模擬原子屬性
模擬原子屬性的核心思想 : 在屬性的
setter方法里面加鎖.但是getter方法里面不加鎖;定義屬性
/// 非原子屬性
@property (nonatomic,strong) NSObject *obj1;
/// 原子屬性:內(nèi)部有"自旋鎖"
@property (atomic,strong) NSObject *obj2;
/// 用于模擬原子屬性
@property (atomic,strong) NSObject *obj3;
- 重寫非原子屬性的
setter和getter方法- 重寫了原子屬性的
setter方法之后,會(huì)覆蓋原子屬性內(nèi)部的自旋鎖,使其失效.然后我們加入互斥鎖,來模擬單寫多讀. - 重寫了屬性的
setter和getter方法之后,系統(tǒng)就不會(huì)再幫我們生成待下劃線的成員變量.使用合成指令@synthesize,就可以手動(dòng)的生成帶下劃線的成員變量.
- 重寫了原子屬性的
3.模擬原子屬性
// 合成指令
@synthesize obj3 = _obj3;
/// obj3的setter方法
- (void)setObj3:(NSObject *)obj3
{
// 使用互斥鎖替代看不見的自旋鎖
@synchronized(self) {
_obj3 = obj3;
}
}
/// obj3的getter方法
- (NSObject *)obj3
{
return _obj3;
}
4.性能測(cè)試
/// 測(cè)試"非原子屬性","互斥鎖","自旋鎖"的性能
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSInteger largeNum = 1000*1000;
NSLog(@"非原子屬性");
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNum; i++) {
self.obj1 = [[NSObject alloc] init];
}
NSLog(@"非原子屬性 => %f",CFAbsoluteTimeGetCurrent()-start);
NSLog(@"原子屬性");
start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNum; i++) {
self.obj2 = [[NSObject alloc] init];
}
NSLog(@"原子屬性 => %f",CFAbsoluteTimeGetCurrent()-start);
NSLog(@"模擬原子屬性");
start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNum; i++) {
self.obj3 = [[NSObject alloc] init];
}
NSLog(@"模擬原子屬性 => %f",CFAbsoluteTimeGetCurrent()-start);
}
測(cè)試結(jié)果
5.互斥鎖和自旋鎖對(duì)比
共同點(diǎn)
- 都能夠保證同一時(shí)間,只有一條線程執(zhí)行鎖定范圍的代碼
不同點(diǎn)
-
互斥鎖:如果發(fā)現(xiàn)有其他線程正在執(zhí)行鎖定的代碼,線程會(huì)進(jìn)入休眠狀態(tài),等待其他線程執(zhí)行完畢,打開鎖之后,線程會(huì)重新進(jìn)入就緒狀態(tài).等待被CPU重新調(diào)度. -
自旋鎖:如果發(fā)現(xiàn)有其他線程正在執(zhí)行鎖定的代碼,線程會(huì)以死循環(huán)的方式,一直等待鎖定代碼執(zhí)行完成.
6.開發(fā)建議
- 所有屬性都聲明為
nonatomic,原子屬性和非原子屬性的性能幾乎一樣. - 盡量避免多線程搶奪同一塊資源.
- 要實(shí)現(xiàn)線程安全,必須要用到
鎖.無論什么鎖,都是有性能消耗的. - 自旋鎖更適合執(zhí)行非常短的代碼.死循環(huán)內(nèi)部不適合寫復(fù)雜的代碼.
- 盡量將加鎖,資源搶奪的業(yè)務(wù)邏輯交給服務(wù)器端處理,減小移動(dòng)客戶端的壓力.
- 為了流暢的用戶體驗(yàn),UIKit類庫的線程都是不安全的,所以我們需要在主線程(UI線程)上更新UI.
- 所有包含
NSMutable的類都是線程不安全的.在做多線程開發(fā)的時(shí)候,需要注意多線程同時(shí)操作可變對(duì)象的線程安全問題.
(八)NSThread線程間通信
1.ATS
使用http地址時(shí)Xcode會(huì)認(rèn)為不夠安全從而保存,為解決此問題需要在info文件的Xml文件內(nèi)添加下列代碼
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
2.代碼實(shí)現(xiàn)
定義屬性
@interface ViewController ()
/// 滾動(dòng)視圖
@property (nonatomic,strong) UIScrollView *scrollView;
/// 圖片視圖
@property (nonatomic,weak) UIImageView *imageView;
@end
loadView 方法復(fù)習(xí)
- 當(dāng)
self.view == nil時(shí),會(huì)調(diào)用; - 先于
viewDidLoad調(diào)用; - 一旦重寫了這個(gè)方法,
storyboard里面就不會(huì)去加載根視圖了;
加載視圖層次
- (void)loadView
{
// 創(chuàng)建滾動(dòng)視圖
self.scrollView = [[UIScrollView alloc] initWithFrame:[UIScreen mainScreen].bounds];
// 將滾動(dòng)視圖設(shè)置成根視圖
self.view = self.scrollView;
self.scrollView.backgroundColor = [UIColor redColor];
// 創(chuàng)建圖片視圖
UIImageView *imageView = [[UIImageView alloc] init];
[self.view addSubview:imageView];
self.imageView = imageView;
}
異步下載圖片
- (void)viewDidLoad {
[super viewDidLoad];
// 主線程中下載圖片
// [self downloadImageData];
// 開啟新線程異步下載圖片
[self performSelectorInBackground:@selector(downloadImageData) withObject:nil];
}
下載圖片主方法
- (void)downloadImageData
{
// 圖片資源地址
NSURL *url = [NSURL URLWithString:@"http://h.hiphotos.baidu.com/image/pic/item/c995d143ad4bd1130c0ee8e55eafa40f4afb0521.jpg"];
// 所有的網(wǎng)絡(luò)數(shù)據(jù)都是以二進(jìn)制的形式傳輸?shù)?所以用NSData來接受
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 回到主線程更新UI
// waitUntilDone:是否等待主線程中的`updateUIwWithImage`方法執(zhí)行結(jié)束再執(zhí)行"下一行代碼",一般設(shè)置成NO,不用等待
[self performSelectorOnMainThread:@selector(updateUIwWithImage:) withObject:image waitUntilDone:NO];
// 測(cè)試 waitUntilDone:
NSLog(@"下一行代碼");
}
刷新UI
- (void)updateUIwWithImage:(UIImage *)imgae
{
NSLog(@"updateUIwWithImage");
// 設(shè)置圖片視圖
self.imageView.image = image;
// 設(shè)置圖片視圖的大小跟圖片一般大
[self.imageView sizeToFit];
// 設(shè)置滾動(dòng)視圖的滾動(dòng):滾動(dòng)范圍跟圖片一樣大
[self.scrollView setContentSize:image.size];
}
線程間通信
- 因?yàn)槎嗑€程共享地址空間和數(shù)據(jù)空間
所以一個(gè)線程的數(shù)據(jù)可以直接提供給其他線程使用,叫做線程間通信;