- 多線程概念
- NSThread:基本使用 現成狀態 資源搶奪 互斥鎖&原子屬性 線程間通訊
- GCD:隊列&任務 GCD常用代碼&隊列的選擇 其他功能(延時,一次性執行,分組)
- NSOperation:簡單使用 其他功能(最大併發數,隊列的暫停&繼續,任務的依賴關係) 網絡圖像加載實現(無沙盒緩存,沙盒緩存,SDWebImage實現原理)
多線程基礎
什麼是進程?
- 進程是指系統中正在運行的一個應用程序
- 每個進程之間是獨立的,每個進程均運行在其專用且受保護的內存空間內
什麼是線程
- 1個進程要想執行任務,必須得有線程(每個進程至少要有1條線程)
- 線程是進程的基本執行單元,一個進程(程序)的所有任務都在線程中執行
比如使用酷我音樂、使用迅雷下載電影,都需要在線程中執行
線程的串行
1個線程中任務的執行是串行(順序執行)的
如果要在1個線程中執行多個任務,那麼只能一個一個地按順序執行這些任務
也就是說,在同一時間內,1個線程只能執行一個任務
比如在1個線程中下載3個文件(分別是文件A,文件B,文件C)
因此,也可以認爲線程是進程中的1條執行路徑
多線程
1.什麼是多線程?
1個進程中可以開啓多條線程,每條線程可以併發(同時)執行不同的任務
進程 -> 車間,線程 -> 車間工人
多線程技術可以提高程序的執行效率
比如同時開啓3條線程分別下載3個文件(分別是文件A,文件B,文件C)
2.多線程的原理
同一時間,CPU只能處理1條線程,只有1條線程在工作(執行)
多線程併發(同時)執行,其實是CPU快速地在多條線程之間調度(切換)
如果CPU調度線程的時間足夠快,就造成了多線程併發執行的假象
思考:如果線程非常非常多,會發生什麼情況?
CPU會在N多線程之間調度,CPU會累死,消耗大量的CPU資源
每條線程被調度執行的頻次會降低(線程的執行效率降低)
多線程的優缺點
1.多線程的優點
能適當提高程序的執行效率
能適當提高資源的利用率(CPU,內存利用率)
2.多線程的缺點
開啓線程需要佔用一定的內存空間(默認情況下,主線程佔用1M,子線程佔用512K),如果開啓大量的線程,會佔用大量的內存空間,降低程序的性能
線程越多,CPU在調度線程上的開銷就越大
程序設計更加複雜:比如線程之間的通信、多線程的數據共享
多線程在iOS開發中的應用
1.什麼是主線程?
一個iOS程序運行後,默認會開啓1條線程,成爲“主線程”或“UI線程”
2.主線程的主要作用
顯示/刷新UI界面
處理UI事件(比如點擊事件、滾動事件、拖拽事件等)
3.主線程的使用注意
別將比較耗時的操作放到主線程中
耗時操作會卡住主線程,嚴重影響UI的流暢度,給用戶一種“卡”的壞體驗
如果將耗時操作放在主線程
4.耗時操作的執行
如果將耗時操作放在子線程(後臺線程,非主線程)
iOS中多線程的實現方案
NSThread的創建
三種創建線程方式
- (void)test1
{
// 實例化一個線程對象
NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
//讓線程開始工作,啓動線程,在新開的線程中執行run方法
[thread start];
}
- (void)test2
{
NSLog(@"%@",[NSThread currentThread]);
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"hello"];
NSLog(@"test1 ---- %@",[NSThread currentThread]);
}
- (void)test3
{
// "隱式"創建線程方法
[self performSelectorInBackground:@selector(run:) withObject:@"hello"];
}
線程的屬性
name:可以給NSThread的對象設置name屬性,這個屬性在當程序有bug打印log時,幫主我們識別是哪個線程中出了問題
threadPriority:優先級,介於0.0~1.0之間,默認值0.5,優先級高的會優先執行。但是在測試時哪個線程優先體現得不是很明顯,多次測試可以觀察到。
#pragma mark - 線程的屬性
- (void)test4
{
NSThread * threadA = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"hello"];
// 給thread設置name屬性,可以在程序出bug時打印的log裏面看出是哪個線程的問題。
threadA.name = @"thread A";
//線程優先級
// 是一個浮點數,0.0~1.0.默認值0.5
// 開發的時候,一般不去修改優先級的值。
// 優先級,必須調用很多次的時候,才能體現出來
threadA.threadPriority = 0.1;
//開始工作
[threadA start];
NSThread * threadB = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"hello"];
threadB.name = @"thread B";
//線程優先級
// 是一個浮點數,0.0~1.0.默認值0.5
threadB.threadPriority = 1.0;
//開始工作
[threadB start];
}
線程的狀態
- 新建一個線程,被放進一個可調度線程池中(這個線程池中有新建的線程以及其他線程)
- start之後進入就緒狀態(Runnable),隨時可被CPU調度
- CPU調度之後進入運行(Running)狀態,如果CPU調度其他線程,則新建線程進入就緒狀態
- 如果線程在運行中,被調用了sleep方法或者等待同步鎖,則進入阻塞(blocked)狀態,阻塞狀態下線程被從可調度線程池中移除,CPU不會調用可調度線程池之外的線程。sleep到時或者得到同步鎖,線程再次進入就緒狀態,即進入可調度線程池
- 線程任務執行完畢或者異常、強制退出時,線程死亡(dead),就從內存中被銷燬
控制線程狀態
1.啓動線程
- (void)start;
//進入就緒狀態->運行狀態。當線程任務執行完畢,自動進入死亡狀態
2.阻塞(暫停)線程
- (void)sleepUntilData:(NSDate*)date;
- (void)sleepForTimeInterval:(NSTimeInteval)ti;
//進入阻塞狀態
3.強制停止線程
+(void)exit;
//進入死亡狀態
注意:一旦線程停止(死亡)了,就不能再次開啓任務
示例代碼:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self test];
}
- (void)test
{
//1.新建一個線程
NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
//2.放進一個可調度線程池,等待被調度。就緒狀態
[thread start];
}
- (void)run
{
NSLog(@"%s",__func__);
//剛進來就睡會
// [NSThread sleepForTimeInterval:2.0];
// 睡到指定的時間點
// [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]];
for(int i = 0;i < 20; i++){
//滿足某一個條件以後,阻塞線程的執行.也就是讓線程休息一會
if(i == 10){
[NSThread sleepForTimeInterval:3.0];
}
//一旦達到某一個條件,就強制終止線程的執行
if(i == 15){
//一旦強制終止,就再也不能重新啓動了
//一旦強制終止,後面的代碼都不會執行了。
[NSThread exit];
}
NSLog(@"%@ ----- %d",[NSThread currentThread],i);
}
NSLog(@"線程結束"); // 線程退出之後這句代碼就執行不到了
}
多線程的安全隱患
1.資源共享
1塊資源可能會被多個線程共享,也就是多個線程可能會訪問同一塊資源
比如多個線程訪問同一個對象、同一個變量、同一個文件
當多個線程訪問同一塊資源時,很容易引發數據錯亂和數據安全問題
安全隱患示例-01
安全隱患示例02-賣票
示例代碼
#import "ViewController.h"
@interface ViewController ()
/**
總票數
*/
@property(nonatomic, assign)int tickets;
@end
@implementation ViewController
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
//剩餘20張票
self.tickets = 20;
//主線程工作
// [self saleTickets];
//增加子線程,賣票
NSThread * threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
threadA.name = @"售票員A";
[threadA start];
NSThread * threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
threadB.name = @"售票員B";
[threadB start];
}
#pragma mark - 賣票
/**
1.開發比較複雜的多線程程序時,可以先再主線程把功能實現
2.實現功能以後,可以把耗時的功能再放到子線程。
3.再增加一個線程,建議開發的時候,線程一個一個增加
*/
- (void)saleTickets
{
while (YES) {
//模擬一下延時,賣一張休息1秒
// [NSThread sleepForTimeInterval:1.0];
//1.判斷是否有餘票
if(self.tickets > 0){
//2.如果有票,就賣一張
self.tickets--;
NSLog(@"剩餘的票數 --- %d ---- %@",self.tickets,[NSThread currentThread]);
}else{
//3.如果沒有就返回
NSLog(@"沒票了");
break;
}
}
}
@end
運行結果
安全隱患分析
安全隱患解決 - 互斥鎖
示例代碼:
#import "ViewController.h"
@interface ViewController ()
/**
總票數
*/
@property(nonatomic, assign)int tickets;
@end
@implementation ViewController
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
//剩餘20張票
self.tickets = 20;
//主線程工作
// [self saleTickets];
//增加子線程,賣票
NSThread * threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
threadA.name = @"售票員A";
[threadA start];
NSThread * threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
threadB.name = @"售票員B";
[threadB start];
}
#pragma mark - 賣票
/**
1.開發比較複雜的多線程程序時,可以先再主線程把功能實現
2.實現功能以後,可以把耗時的功能再放到子線程。
3.再增加一個線程,建議開發的時候,線程一個一個增加
*/
/**
加鎖,互斥鎖
加鎖,鎖定的代碼儘量的少
加鎖範圍內的代碼,同一時間只允許一個線程執行
互斥鎖的參數:任何繼承自NSObject *對象
要保證這個鎖,所有的線程都能訪問到,並且所有線程訪問的是同一個鎖對象
*/
- (void)saleTickets
{
while (YES) {
//模擬一下延時,賣一張休息1秒
// [NSThread sleepForTimeInterval:1.0];
// NSObject * obj = [[NSObject alloc] init];
//爲什麼沒有提示?因爲蘋果不推薦是用加鎖。因爲加鎖,性能太差。
@synchronized(self){ //開發的時候,一般就是用self就ok
//1.判斷是否有餘票
if(self.tickets > 0){
//2.如果有票,就賣一張
self.tickets--;
NSLog(@"剩餘的票數 --- %d ---- %@",self.tickets,[NSThread currentThread]);
}else{
//3.如果沒有就返回
NSLog(@"沒票了");
break;
}
}
}
}
@end
1.互斥鎖是用格式
@synchronized(鎖對象){//需要鎖定的代碼}
注意:鎖定一份代碼只用1把鎖,用多把鎖是無效的
2.互斥鎖的優缺點
優點:能有效防止因多線程搶奪資源造成的數據安全問題
缺點:需要消耗大量的CPU資源
3.互斥鎖的是用前提:多條線程搶奪同一資源
4.相關專業術語:線程同步
線程同步的意思是:多條線程按順序執行任務
互斥鎖,就是使用了線程同步技術
原子屬性&線程安全
@property (atomic,strong)NSObject * obj;
nonatomic:非原子屬性
atomic:原子屬性 – 默認屬性
原子屬性就是針對多線程設計的。原子屬性實現單(線程)寫多(線程)讀
因爲寫的安全級別要求更高。讀的要求低一些,可以多讀幾次來保證數據的正確性
atomic情況下,只要重寫了set方法,getter也得重寫
- (void)setObj:(NSObject *)obj
{
// 原子屬性內部使用的自旋鎖
// 自璇鎖和互斥鎖
// 共同點:都可以鎖定一段代碼。同一時間,只有線程能夠執行這段鎖定的代碼
// 區別:互斥鎖,在鎖定的時候,其他線程會睡眠,等待條件滿足,再喚醒
// 自旋鎖,在鎖定的時候,其他的線程會做死循環,一直到等待條件滿足,一旦條件滿足,立馬去執行,少了一個喚醒過程
//互斥鎖,
@synchronized(self){ //模擬鎖。真實情況下,使用的不是互斥鎖
_obj = obj;
}
}
如果同時重寫了setter和一個getter方法,”_成員變量”就不會提供,可以使用@synthesize合成指令,告訴編譯器屬性的成員變量的名稱
@synthesize obj = _obj;
- (NSObject *)obj
{
return _obj;
}
線程安全的概念
就是在多個線程同時執行的時候,能夠保證資源信息的準確性
‘UI線程’ – 主線程
** UIKit 中絕大部分的類,都不是“線程安全”的
怎麼解決這個線程不安全的問題?
蘋果約定,所有程序的更新UI都在主線程進行,也就不會出現多個線程同時改變一個資源
在主線程更新UI,有什麼好處
1.只在主線程更新UI,就不會出現多個線程同時改變 同一個UI控件
2.主線程的優先級最好。也就意味着UI的更新優先級高。會讓用戶感覺很流暢