術語
任務:準備執行的操作,通常就是一個block塊
隊列:queue,存放任務,管理任務
進程——蘋果電腦里的活動監視器App里詳細羅列了操作系統此時此刻正在運行的所有程序,同時標明了每一個進行的程序的所有線程。
線程——將一個程序都轉換成匯編的CPU命令時,由一堆不分叉的Cpu指令組成的路徑(主線程命令和分線程命令)
多線程編程——由多條不分叉的CPU指令所組成的路徑就是多線程編程。優點:提高資源利用率和程序執行效率,用戶體驗 缺點:1、上下文切換頻率太高會影響性能 2、資源競爭—>上鎖(類似單例)3、死鎖 4、開辟的線程其實類似于創建一個對象,會消耗大量內存(內核數據結構(大約1KB)、棧空間(子線程512KB、主線程1MB,也可以使用-setStackSize:設置,但必須是4K的倍數,而且最小是16K),創建線程大約需要90毫秒的創建時間)5、程序設計更加復雜:比如線程之間的通信、多線程的數據共享
CPU包括——1、物理CPU(硬件)2、CPU核 (一個物理CPU可以虛擬出多個CPU核,一個CPU核就相當于一個CPU的功能,可以分身,一個CPU核一個時刻只能執行一個CPU命令)
并發——雖然同一時間,CPU只能處理1條線程,只有1條線程在執行,但是CPU可以快速地在多條線程之間調度切換,如果CPU調度線程的時間足夠快,就造成了多線程并發執行的假象。當然如果線程過多,會消耗大量的CPU資源,同時每條線程被調度執行的頻次會降低(線程的執行效率降低)
多核就快
首先談一談就是程序執行的本質,我們寫的代碼都首先是轉化成一行行的匯編代碼才能被計算機所識別。這一行行的匯編代碼其實也叫做CPU指令。這里面就涉及到CPU了,CPU就是讀取這些指令的工具。當然,有一種匯編代碼也就是CPU指令的結構是從頭到尾不間斷的,也就是這些CPU指令一點也沒有分叉,從頭到尾都是直路,就像高速公路一樣,只有直路,沒有岔路。可是程序的魅力就在于,遇到分岔路時或更復雜的十字路口時應該怎么辦?肯定不能一根筋呀!于是CPU廠商就想到了一個方法,就是分身術,當遇到分叉路的時候就像孫悟空一樣,復制一個自己來走新出現的分岔路。可是問題來了,如果第二次又遇到分叉路怎么辦呢?兩種思路,第一種再復制一個自己。第二種就是用那個復制的自己輪流前進。第一種方法主要受到CPU本身性能的局限。有的CPU只能復制一個自己,算上本身總共才兩個。也有的CPU可以復制三個自己,算上本身總共有4個。這種具體去跑馬路的車就叫做CPU核,所以你會發現,CPU越強大,也就是能夠復制的自己越多,能夠同時走的分叉路就越多,自然而然,四核就是比雙核快。第二種方法就是讓CPU的寄存器不斷受到考驗,當CPU分配那個復制的自己在那一條道路上奔跑時,必須時時記錄下來這個復制的自己已經在每一條分叉路上已經跑了多遠,并需要準確無誤的保存在寄存器中,然后當CPU再一次將那個復制的自己放在相應地分岔路上繼續跑時,就會讀取寄存器里面的數據來繼續前進。
主線程刷UI
因為分線程不能刷新UI,但是有的時候明明看見刷新UI的代碼寫在了分線程里,但這并不代表這些代碼會被分線程(復制的CPU核)執行,分線程主要是負責數據的下載。(兩種情況:1、下載完成后主動回到主線程,告訴主線程自己已經完成下載,可以讓主線程執行分線程沒有資格執行的代碼 2、需要主線程不定期去主動檢查也就是說,盡管你把刷新UI的代碼寫在了分線程,但分線程只會執行其中的下載代碼,根本不會執行刷新UI的代碼。刷新UI的代碼會等到主線程不定期的主動檢查分線程時才會被執行。所以這樣會出現一些延時,所以應該在下載完成后主動回到主線程)
時間片
如果同一個CPU核去管理多個線程時,就會涉及到時間分配的問題,如果管理的是兩個線程,線程一有10個命令,線程二有20個命令,CPU核會將時間片分配給兩個線程,一一去執行完兩個線程里面的所有命令,當一個線程切換到另一個線程時,系統會將當前線程的信息保存到寄存器中,等下次切換過來到時候喚醒寄存器,讀取數據,從上一次的命令接著處理。這就是上下文切換。上下文切換——時間片分配(隨機分配CPU核給那一個分線程,同時保存每一分線程上一次執行到的位置到寄存器中,告訴CPU核現在該從第幾個CPU指令開始執行)
NSThread
- 創建、啟動線程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
- 線程相關用法
// 獲得當前線程
NSThread *current = [NSThread currentThread];
// 線程名字
- (void)setName:(NSString *)name;
- (NSString *)name;
// 獲得主線程
+ (NSThread *)mainThread;
// 判斷是否主線程
- (BOOL)isMainThread;
+ (BOOL)isMainThread;
// 創建線程后自動啟動
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
// 隱式創建并啟動線程
[self performSelectorInBackground:@selector(run) withObject:nil];
// 阻塞(暫停)線程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 強制停止線程
+ (void)exit;
GCD全稱Grand Central Dispatch
純C語言函數,為多核并行運算而生,自動利用更多的CPU內核(比如雙核、四核),且自動管理線程的生命周期(創建線程、調度任務、銷毀線程),只需告訴GCD想要執行的任務,根本無需寫任何管理線程生命周期的代碼。隊列管理任務執行順序,同步異步決定任務執行線程。
- GCD創建并發隊列
// 創建并發隊列(隊列名稱,隊列類型)
dispatch_queue_t myQueue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
// 獲得默認并發隊列(隊列優先級,缺省參數0)
dispatch_queue_t systemQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 任務加入并發隊列
dispatch_async(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"3-----%@", [NSThread currentThread]);
});
- GCD創建串行隊列
// 創建串行隊列(隊列名稱,隊列類型)
dispatch_queue_t myQueue = dispatch_queue_create("串行隊列", DISPATCH_QUEUE_SERIAL);
// 直接獲取主隊列 = 特殊串行隊列
dispatch_queue_t systemQueue = dispatch_get_main_queue();
// 任務加入串行隊列
dispatch_sync(queue, ^{
NSLog(@"1-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"2-----%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"3-----%@", [NSThread currentThread]);
});
- GCD同步執行任務
// 繼續當前線程(隊列,任務)
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
- GCD異步執行任務
// 開啟新線程(隊列,任務)
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
- GCD線程通信
// 從子線程回到主線程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 執行耗時的異步操作...
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主線程,執行UI刷新操作
});
});
// 對比performSelector的線程通信
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
- GCD延時執行
// GCD延時
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2秒后異步執行這里的代碼...
});
// 對比performSelector延時
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// 對比NSTimer延時
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:NO];
- GCD單例執行
// 保證某段代碼在程序運行過程中只被執行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只執行1次的代碼(這里面默認是線程安全的)
});
- GCD創建定時器執行代碼塊
// 創建Timer
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
// 設置定時器的觸發時間(1秒后)和時間間隔(每隔2秒)
dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), 2 * NSEC_PER_SEC, 0);
// 設置Block回調
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"Timer %@", [NSThread currentThread]);
});
// 開啟定時器
dispatch_resume(self.timer);
// 取消置空定時器
dispatch_cancel(self.timer);
self.timer = nil;
- GCD實現任務依賴
// 隊列組管理任務一和任務二
dispatch_group_t group = dispatch_group_create();
// 異步執行任務一
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});
// 異步執行任務二
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});
// 任務一和任務二都結束Block回調
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
});
- GCD分線程遍歷
/**
* GCD快速異步遍歷
*/
- (void)apply
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSString *from = @"初始路徑";
NSString *to = @目標路徑";
NSFileManager *mgr = [NSFileManager defaultManager];
NSArray *subpaths = [mgr subpathsAtPath:from];
dispatch_apply(subpaths.count, queue, ^(size_t index) {
NSString *subpath = subpaths[index];
NSString *fromFullpath = [from stringByAppendingPathComponent:subpath];
NSString *toFullpath = [to stringByAppendingPathComponent:subpath];
[mgr moveItemAtPath:fromFullpath toPath:toFullpath error:nil]; // 剪切
NSLog(@"%@---%@", [NSThread currentThread], subpath);
});
}
/**
* 對比傳統異步遍歷
*/
- (void)moveFile {
NSString *from = @"初始路徑";
NSString *to = @目標路徑";
NSFileManager *mgr = [NSFileManager defaultManager];
NSArray *subpaths = [mgr subpathsAtPath:from];
for (NSString *subpath in subpaths) {
NSString *fromFullpath = [from stringByAppendingPathComponent:subpath];
NSString *toFullpath = [to stringByAppendingPathComponent:subpath];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[mgr moveItemAtPath:fromFullpath toPath:toFullpath error:nil]; // 剪切
});
}
}
NSOperation&&NSOperationQueue
NSOperation是個并不具備封裝操作能力的抽象類,必須使用它的子類來封裝操作,NSOperation子類包括NSInvocationOperation、NSBlockOperation、自定義子類繼承NSOperation。如果僅僅是實例化了一個操作對象,然后簡簡單單的執行這個操作的start方法,這樣沒有絲毫意義,還不如在.m文件里面寫一個私有方法來得直接,可是一旦把實例化的NSOperation對象放到NSOperationQueue隊列之中,添加到隊列里面的操作對象會自動開始調用操作對象的start方法,開始異步在一條新的線程里面執行封裝在操作對象里面的一系列代碼,這就實現了分線程操作了,好神奇有木有!
- 創建NSInvocationOperation操作
// 創建NSInvocationOperation對象
- (id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;
// 默認同步,除非添加操作到隊列
- (void)addOperation:(NSOperation *)op;
- 創建NSBlockOperation操作
// 創建NSBlockOperation
+ (id)blockOperationWithBlock:(void (^)(void))block;
// 添加更多操作(只要任務數>1,就會異步執行操作)
- (void)addExecutionBlock:(void (^)(void))block;
- 創建自定義NSOperation操作
重寫父類NSOperation的main方法,在main方法里面寫入我想要封裝的執行的任務,但是有一點需要特別注意,就是自己寫的這段代碼很可能會是在子線程里面異步執行,既然是異步操作,自然無法訪問主線程的自動釋放池,對象的內存釋放便是需要重點考慮的內容。經常的做法就是,在封裝想要執行的代碼之前,必須先判斷一下這個操作對象是否已經被取消,因為一旦開始執行進入了子線程就無法在判斷isCancelled這個屬性了,所以必須在開始執行封裝的代碼之前,必須先判斷這個操作對象是否已經被取消。如果判斷已經取消,我需要做的就是一些內存的管理。
- 創建NSOperation操作依賴
// 一定先讓操作A執行完畢,然后才能執行操作B
[operationB addDependency:operationA];
!!!一定注意不能相互依賴
- 監聽NSOperation操作執行完畢
- (void (^)(void))completionBlock;
- (void)setCompletionBlock:(void (^)(void))block;
- 添加操作到NSOperationQueue對列
- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
- 設置操作對列NSOperationQueue最大并發數
- (NSInteger)maxConcurrentOperationCount;
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;
- 對列NSOperationQueue的取消、暫停、恢復
// 取消隊列操作
- (void)cancelAllOperations;
- (void)cancel;
// 暫停Or恢復隊列
- (void)setSuspended:(BOOL)bool;
- (BOOL)isSuspended;
GCD和NSOperation的區別?
GCD無法控制線程的最大并發數,而NSOperation可以控制同時進行的線程個數!
開辟多線程?
- performSelector
作為NSObject
的類別,因此只要是控制器的對象就可以直接調用這個方法。而且不僅僅可以通過performSelectorInBackground
將耗時操作放進分線程里,更可以通過performSelectorOnMainThread
將分線程里的參數傳遞到主線程以便進一步操作,例如刷新UI
。
- NSThread線程類
thread
本身就是線的意思。通過NSThread
類實例化一個對象。然后直接通過初始化方法就可以像為UIButton
增添點擊事件那樣增添一個在分線程里進行的方法。而且可以傳遞給這個分線程方法一個無論什么對象類型的參數。但是特別注意:通過NSThread
類實例化的對象所初始化增添的分線程方法必須通過[對象 start]方法表示開始執行分線程方法。而且無論第三方工具如何豐富,本質上都是對NSThread
進行一系列操作。所以說在任何情況下都可以調用[NSThread currentThread]
這個類方法獲取到當前的線程信息,有點像代碼版的活動監視器哈!這是蘋果提供的三種方法里面相對輕量級的,但需要管理線程的生命周期、同步、加鎖的問題,這會導致一定得性能開銷。
- GCD隊列Grand Central Dispatch
偉大的中樞調度器,充分發揮CPU內核的性能,自動管理線程的生命周期,包括創建線程,調度任務和銷毀線程。而且使用過程中只需添加任務即可,無需管理線程。
- NSOperation
一塊資源可能會被多個線程共享,通常添加互斥鎖@synchronized(self){}
來避免引發數據錯亂和數據安全問題。但是添加互斥鎖會影響手機的性能,所以盡量將加鎖和資源搶奪業務的邏輯交給服務器端處理,線程不會孤立存在,子線程下載圖片,在主線程刷新UI顯示圖片。使用@synchronized(self的鎖對象){}
包起來的代碼同一時刻只能被一個線程執行,而且加鎖的時候盡量縮小范圍,范圍越大就越消耗性能。
串行隊列Vs并行隊列?
相同點:
創建方式都是
dispatch_queue_t queue
,只不過創建隊列時的標志符后面的參數有了一些變化,主要有(NULL, DISPATCH_QUEUE_SERIAL,DISPATCH_QUEUE_CONCURRENT)
三種類型。前兩種等價都可以表示串行隊列。而且有一個細節就是標識符的格式是否有要求一時確定不了!往隊列里添加任務的方式一模一樣,都是
dispatch_async(queue, ^{})
這種格式。凡是創建的隊列都是以分線程的形式存在。所以對于一個分線程隊列來說就必須考慮如何返回到主線程。而且這也不是說純粹意義上的將這個分線程注銷,更多的是想將無法在分線程中執行的代碼及時的傳遞到主線程以便進行下一步操作。將分線程中下載完成的數據如何讓以參數的形式傳遞到主線程呢?兩種方法,方法1就是使用控制器的對象調用
performSelectorOnMainThread
方法。方法2就是通過dispatch_queue_t mainQueue = dispatch_get_main_queue()
獲取到主隊列,這也就代表了主線程。通過在主隊列中添加任務也就等于是通過主線程來執行分線程無法執行的代碼。而且方法2還有一個優點就是,無需像方法一那樣過于糾結于到底應該將分線程的什么參數傳遞出去,因為方法二是直接建立在分線程的基礎上,本質上只是起一個臨時調用主線程的功能,因此方法二可以直接調用分線程里的任何參數變量。如果不理解什么是串行隊列,其實主隊列
dispatch_queue_t mainQueue = dispatch_get_main_queue()
就是一個典型的串行隊列,無論你添加多少任務總是按照任務的添加順序依次執行,沒有例外。而且自始至終都是一個線程,而且這個線程就叫做主線程。
不同點:
串行隊列無論添加多少個任務都只是創建了一個分線程,而且執行任務的順序嚴格按照添加任務的先后順序進行。而對于并行隊列而言,往并行對列中每增添一個任務就創建了一個分線程。也就是說并行隊列中有多少個任務就有多少個分線程,我們也都知道,線程其實本質上也是一個對象,是對象就必須開辟內存,因此雖然說創建很多的分線程有利于獲得良好的用戶體驗,但這對于用戶終端的內存是一個極大的考驗。
并行隊列的任務因為是一個任務對應一個分線程。所以可以認為并行隊列里的任務是同時進行的。所以最終并行隊列里的哪一個任務最先執行結束的關鍵還在于任務的具體內容。
對于并行隊列而言有一種很特殊的需求就是:我想準確地知道并行隊列里的最后一個任務是什么時候執行完畢,然后我再進行下一部操作,因為通常情況下是無法準確地獲取到現在并行隊列的任務到底執行到了一個什么階段。根本無法像串行隊列那樣通過添加任務的先后順序來推斷出到底哪一個任務結束后標志著隊列任務全部結束!解決方案就是給并行隊列添加一個
group
。同樣需要通過dispatch_queue_t queue = dispatch_queue_create("123", DISPATCH_QUEUE_CONCURRENT)
先創建一個并行隊列,然后通過dispatch_group_t group = dispatch_group_create()
創建一個管理并行隊列里所有任務的組。接著跟常規添加任務到并行隊列不同的是:dispatch_group_async(group, queue, ^{});
當然從分線程返回到主線程的方法dispatch_async(dispatch_get_main_queue(), ^{});
并沒有發生變化。把所有的任務添加到有管理組的并行隊列中目的就是時刻監聽并行隊列里的任務的進行階段,監聽到并行隊列里所有的任務全部結束時就會觸發方法dispatch_group_notify(group,dispatch_get_main_queue(),^{});
需要注意的是,這個方法里面的代碼全部是由主線程執行。因為并行隊列里的所有任務所對應的分線程也已經結束。監聽到任務結束自動將監聽方法里面的代碼交由主線程執行。這也就是說,其實管理并行隊列里所有任務的組group
的監聽方法其實本質上就是就是并行隊列里所有任務都結束后返回到主線程以便進行下一步操作。如果我們想要創建多個并行對列,那么又如何確定哪一個并行隊列率先執行呢,答案是,根據并行隊列的優先級來進行判斷。創建有優先級的并行隊列的方法為dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)。當然在這里將并行隊列的優先級設置為了默認,也就相當于又增加了一種創建普通并行隊列的方法。
如果我想隊列里的任務在多少秒之后才開始執行,那么最簡單的方法就是在具體的任務里添加睡眠延時
sleep(1)
,但是的但是這也僅僅適用于串行隊列中,而且就算這樣可以也會帶來一個整體任務進行的延時。對于并行隊列來說,只要添加完了任務就會全面開始,根本不會停下來,所以最好的方法就是通過dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 4ull*NSEC_PER_MSEC)
來設置過4秒后再通過dispatch_after(time, queue, ^{})
將任務添加到隊列queue
中。這樣就可以完美的實現至少在多少秒后再進行某一個任務。6、無論在串行隊列還是在并行隊列中,有時候我們想要某個任務只進行一次,就算這是一個死循環的隊列,也要保證某個任務只被執行一次,方法就是先用static dispatch_once_t onceTaken
確定謂詞onceTaken
,用來保證dispatch_once(&onceTaken,^{ })
方法里的代碼也就是任務將從始至終只會被執行一次。
互斥鎖
@synchronized(鎖對象) { // 需要鎖定的代碼 }
static id _instance;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
@synchronized(self) {
if (_instance == nil) {
_instance = [super allocWithZone:zone];
}
}
return _instance;
}
+ (instancetype)sharedInstance
{
@synchronized(self) {
if (_instance == nil) {
_instance = [[self alloc] init];
}
}
return _instance;
}
- (id)copyWithZone:(NSZone *)zone
{
return _instance;
}
優點:能有效防止因多線程搶奪資源造成的數據安全問題
缺點:需要消耗大量的CPU資源
前提:多條線程搶奪同一塊資源
線程同步:多條線程在同一條線上執行(按順序地執行任務)
atomic:原子屬性,為setter方法加鎖(默認就是atomic),線程安全,需要消耗大量的資源
nonatomic:非原子屬性,不會為setter方法加鎖,非線程安全,適合內存小的移動設備,盡量避免多線程搶奪同一塊資源,盡量將加鎖、資源搶奪的業務邏輯交給服務器端處理,減小移動客戶端的壓力