[TOC]
一、 線程和進程
1.1 線程的定義
- 線程是進程的
基本執(zhí)行單元,一個進程的所有任務倒在線程中執(zhí)行 - 進程要想執(zhí)行任務,必須得有線程,進程至少要有一條線程
- 程序啟動會默認開啟一條線程,這條線程被稱為主線程或 UI 線程
1.2 進程的定義
- 進程是指在系統(tǒng)中正在運行的一個應用程序
- 每個進程之間是獨立的,每個進程均運行在其專用的且受保護的內存
1.3 iOS開發(fā)為什么是單進程的
- 沙盒-單進程數(shù)據(jù)更加隱私、安全
- 進程切換,消耗資源大
1.4 線程和隊列的關系
- 線程和隊列是兩個完全獨立的概念,線程是由系統(tǒng)分配、系統(tǒng)調度,隊列是用來管理任務,開發(fā)者可以管理任務的串行、并行、優(yōu)先級、先后順序;
- 開發(fā)者管理任務隊列,系統(tǒng)根據(jù)隊列的屬性,來給隊列中的任務分配線程;
- 隊列是提供給開發(fā)者使用的抽象概念,線程是由系統(tǒng)調度的真實對象,開發(fā)者可以通過管理隊列來指定系統(tǒng)分配、調度線程的數(shù)量、順序等。
- 任務執(zhí)行速度:CPU、線程、隊列、任務的復雜度、任務的優(yōu)先級
- 大量的臨時變量:使用 autorealease 處理
1.5 線程和進程的關系和區(qū)別
- 地址空間:同一進程的線程共享本進程的地址空間,而進程之間則是獨立的地址空間
- 資源擁有:同一進程內的線程共享進程的資源,如內存、I/O、cpu等,但是進程之間的資源是獨立的
- 健壯性:一個進程崩潰后,在保護模式下不會對其他進程產生影響,但是一個線程崩潰整個進程都會死掉。多以多進程要比多線程健壯
- 并發(fā)操作:進程切換時,消耗的資源大,效率高。所以涉及到頻繁的切換時,使用線程要好于進程。同樣如果要求同時進行并且又要共享某些變量的并發(fā)操作,只能使用線程不能用進程
- 執(zhí)行過程:每個獨立的進程有一個程序運行的入口、順序執(zhí)行序列和程序入口。但是線程不能獨立執(zhí)行,必須依存于應用程序中,由應用程序提供多個線程執(zhí)行控制
- 調度單位:線程是處理器調度的基本單位,但進程不是
1.6 進程和線程與堆和棧的關系
- 進程的地址空間
每個進程的地址空間是獨立的,是操作系統(tǒng)把物理內存映射到進程的虛擬地址上。
進程只能訪問自己的地址空間,不能訪問別的進程的地址空間。
通俗的理解:如果有a.exe和b.exe同時在系統(tǒng)中運行,那么a和b都可以有0x00000000-0xffffffff的虛擬地址空間(不考慮操作系統(tǒng)占用等因素)。
假如a b都有1個變量在地址0x12345678處,看起來地址一樣,實際上在物理內存中不是一個地方。 -
進程是線程的容器,windows調度運行的單位是線程,一個進程至少有1個主線程。一個進程內所有的線程都處于同一個虛擬地址空間。 -
進程初始化的時候,系統(tǒng)會在進程的地址空間中創(chuàng)建一個堆,叫進程默認堆。進程中所有的線程共用這一個堆。當然,可以增加1個或幾個堆,給不同的線程共同使用或單獨使用。 - 創(chuàng)建線程的時候,系統(tǒng)會在進程的地址空間中分配1塊內存給線程棧,通常是1MB。
線程棧是獨立的,不共享。iOS中是512KB
1.7 補充內存相關知識
內存五大分區(qū)
1、棧區(qū)(stack):由編譯器自動分配釋放,存放函數(shù)的參數(shù)值,局部變量的值等。其操作方式類似于數(shù)據(jù)結構中的棧。
2、堆區(qū)(heap):一般由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收。注意它與數(shù)據(jù)結構中的堆是兩回事,分配方式類似于鏈表。new出來的放在這里。
3、全局區(qū)(靜態(tài)區(qū)):(static)全局變量和靜態(tài)變量的存儲是放在一塊的,初始化的全局變量和靜態(tài)變量在一塊區(qū)域,未初始化的全局變量和未初始化的靜態(tài)變量在相鄰的另一塊區(qū)域。程序結束后由系統(tǒng)釋放。
4、文字常量區(qū):常量字符串就是放在這里的。程序結束后由系統(tǒng)釋放
5、程序代碼區(qū):存放函數(shù)體的二進制代碼。-
棧和堆的區(qū)別
-
管理方式不同。棧由操作系統(tǒng)自動分配釋放,無需我們手動控制;堆的申請和釋放工作由程序員控制,容易產生內存泄漏; -
空間大小不同。每個進程擁有的棧的大小要遠遠小于堆的大小。理論上,程序員可申請的堆大小為虛擬內存的大小,進程棧的大小64bits的Windows默認1MB,64bits的Linux默認10MB; -
生長方向不同。堆的生長方向向上,內存地址由低到高;棧的生長方向向下,內存地址由高到低。 -
分配方式不同。
堆都是動態(tài)分配的,沒有靜態(tài)分配的堆。
棧有2種分配方式:靜態(tài)分配和動態(tài)分配。靜態(tài)分配是由操作系統(tǒng)完成的,比如局部變量的分配。
棧的動態(tài)分配由alloca函數(shù)進行分配,但是棧的動態(tài)分配和堆是不同的,棧的動態(tài)分配是由操作系統(tǒng)進行釋放,無需我們手工實現(xiàn)。 -
分配效率不同。
棧由操作系統(tǒng)自動分配,會在硬件層級對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執(zhí)行,這就決定了棧的效率比較高。
堆則是由C/C++提供的庫函數(shù)或運算符來完成申請與管理,實現(xiàn)機制較為復雜,頻繁的內存申請容易產生內存碎片。顯然,堆的效率比棧要低得多。
-
二、 多線程
2.1 多線程的意義
- 優(yōu)點
- 能適當提高程序的執(zhí)行效率
- 能適當提高資源的利用率(CPU、內存)
- 線程上的任務執(zhí)行完成后,線程會自動銷毀
- 缺點
- 開啟線程需要占用一定的內存空阿金(默認情況下,每一個線程都占 512 KB)
- 如果開啟大量的線程,會占用大量的內存空間,降低程序的性能
- 線程越多,CPU 在調用線程上的開銷就越大
- 程序設計更加復雜,比如線程間的通信、多線程的數(shù)據(jù)共享
2.2 多線程的原理
CPU 在單位時間片里快速在各個線程之間切換
-
單線程
單核 CPU單線程 任務1 任務2 任務3- 其本質也是單線程,一次只能執(zhí)行一個任務
- 單核 CPU的多線程是通過 任務1執(zhí)行一部分再執(zhí)行任務2再切換任務3,這種方式達到多線程的效果
- 不停切換、時間片、共贏、達到利益最大化
-
多線程
多核 CPU線程1 任務1 線程2 任務2 線程3 任務3
2.3 多線程技術方案
-
pthread- 一套通用的多線程API
- 適用于 Unix/Linux/Windows 等系統(tǒng)
- 跨平臺/可移植
- 使用難度大
-
NSThread- 使用更加面向對象
- 簡單易用,可直接操作線程對象
-
GCD- 旨在替代 NSTread 等線程技術
- 充分利用設備的多核
-
NSOperation- 基于GCD(底層是GCD)
- 比 GCD 多了一些更簡單使用的功能
- 使用更加面向對象
三、線程、線程池、生命周期、安全、通訊、runloop
3.1 線程的生命周期
新建:Start,創(chuàng)建線程,調用start進入就緒狀態(tài)
就緒:Runnable,CPU調度當前線程,進入運行狀態(tài)
運行:Running,任務執(zhí)行完畢,退出線程并銷毀,CPU調度當前線程
阻塞:Blocked,調用 sleep方法/等待同步鎖/從可調度線程池移出
死亡:Dead,任務執(zhí)行完成、強制退出
-
可調度線程池
- 當前線程
- 其他線程
- CPU調度的線程是從線程池中獲取
-
正常流程和堵塞流程
- 正常流程:
新建->就緒->運行->死亡 - 堵塞流程:
新建->就緒->運行->堵塞->就緒->...->堵塞
image.png
- 正常流程:
3.2 線程池的原理
- 參數(shù)解釋
- corePoolSize:線程池的基本大?。ê诵木€程池大?。?/li>
- maxinumPoolSize:線程池的最大大小
- keepAliveTime:線程池中超過corePoolSize數(shù)目的空閑線程的最大存活時間
- unit:keepAliveTime參數(shù)的時間單位
- workQueue:任務阻塞隊列
- threadFactory:新建線程的工廠
- handler:當提交的任務數(shù)超過maxnumPoolSize與workQueue之和時,任務會交給RejectedExecutionHandler來處理
- 【原理】線程池大小小于核心線程池大小
- 創(chuàng)建線程執(zhí)行任務,流程和線程的生命周期一樣
- 【原理】線程池大小不小于核心線程池大小
- 線程池判斷工作隊列未滿
將任務push進隊列 - 線程池判斷工作隊列已滿
- 且
maxinumPoolSize>corePoolSize,將創(chuàng)建新的線程來執(zhí)行任務 - 交給報策略去處理
- Abort策略:默認策略,新任務提交時直接拋出未檢查的異常 RejectedExecution,該異??捎烧{用者捕獲
- CallerRuns策略:為調節(jié)機制,既不拋棄任務也不拋出異常,而是將某些任務會退到調用者。不會在線程池的線程中執(zhí)行新的任務,而是在調用exector的線程中運行新的任務
- Discard策略:新提交的任務被拋棄。
- DiscardOldest策略:隊列的是“對頭”的任務,然后嘗試提交新的任務。(不適合工作隊列為優(yōu)先隊列場景)
- 且
- 線程池判斷工作隊列未滿
3.3 線程和runloop的關系
- runloop與線程的對應關系
- runloop與線程是一一對應的,一個runloop對應一個核心的線程;
- 為什么說是核心的?因為runloop是可以嵌套的,但是核心的只能有一個,他們的關系保存在一個全局的字典里;
- runloop是來管理線程的,當線程的runloop被開啟后,線程會在執(zhí)行完成后進入休眠狀態(tài),有了任務就會被喚醒去執(zhí)行任務。
- runloop在第一次或失去時被創(chuàng)建,在線程結束時被銷毀。
- 對于主線程來說,runloop在程序一啟動就默認創(chuàng)建好了。
- 對于子線程來說,runloop是懶加載的,只有當我們使用的時候才會創(chuàng)建,所以在子線程用定時器要注意:確保子線程的runloop被創(chuàng)建,不然定時器不會回調。
3.4 線程安全和線程通訊
-
atomic與nonatomic的區(qū)別- 屬性定義
-
nonatomic:非原子屬性,適合內存小的移動設備 -
atomic:原子屬性(線程安全),針對多線程設計,默認值,需要消耗大量的資源。atomic只保證了寫安全,但對于可變數(shù)組這類容器中的元素的讀寫安全并不能保證。
-
- 線程安全解釋
- 保證同一時間只有一個線程能夠寫入
- atomic本身就有一把鎖(自旋鎖)
- 單寫多讀:單個線程寫入,多個線程可以讀取
- iOS開發(fā)的建議
- 所有屬性都聲明為 nonatomic
- 盡量避免多線程搶奪一塊資源
- 盡量將加鎖、資源搶奪的業(yè)務邏輯交給服務器端處理,減小移動客戶端的壓力
- 屬性定義
-
互斥鎖小結
- 保證鎖內的代碼,同一時間,只有一條線程能夠執(zhí)行
- 互斥鎖的鎖定范圍,應該盡量小,鎖定范圍越大,效率越差
- 互斥鎖參數(shù)
- 能夠加鎖的任意 NSObject 對象
- 注意:鎖對象一定要保證所有的線程都能夠訪問
- 如果代碼中只有一個地方需要加鎖,大多都使用 self,這樣可以避免單獨再創(chuàng)建一個鎖對象
四、iOS中常用多線程技術
4.1 pthread
pthread_create 創(chuàng)建線程
-
參數(shù):
- pthread_t:要創(chuàng)建線程的結構體指針,通常開發(fā)的時候,如果遇到 C 語言的結構體,類型后綴
_t / Ref結尾
同時不需要* - 線程的屬性,nil(空對象 - OC 使用的) / NULL(空地址,C 使用的)
- 線程要執(zhí)行的
函數(shù)地址
void *: 返回類型,表示指向任意對象的指針,和 OC 中的 id 類似
(*): 函數(shù)名
(void *): 參數(shù)類型,void * - 傳遞給第三個參數(shù)(函數(shù))的
參數(shù)- 返回值:C 語言框架中非常常見
int
0 創(chuàng)建線程成功!成功只有一種可能
非 0 創(chuàng)建線程失敗的錯誤碼,失敗有多種可能!
- 返回值:C 語言框架中非常常見
// 1: pthread pthread_t threadId = NULL; //c字符串 char *cString = "HelloCode"; int result = pthread_create(&threadId, NULL, pthreadTest, cString); if (result == 0) { NSLog(@"成功"); } else { NSLog(@"失敗"); }void *pthreadTest(void *para){ // 接 C 語言的字符串 // NSLog(@"===> %@ %s", [NSThread currentThread], para); // __bridge 將 C 語言的類型橋接到 OC 的類型 NSString *name = (__bridge NSString *)(para); NSLog(@"===>%@ %@", [NSThread currentThread], name); return NULL; } - pthread_t:要創(chuàng)建線程的結構體指針,通常開發(fā)的時候,如果遇到 C 語言的結構體,類型后綴
4.2 NSThread
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
/**
1. 循環(huán)的執(zhí)行速度很快
2. 棧區(qū)/常量區(qū)的內存操作也挺快
3. 堆區(qū)的內存操作有點慢
4. I(Input輸入) / O(Output 輸出) 操作的速度是最慢的!
* 會嚴重的造成界面的卡頓,影響用戶體驗!
* 多線程:開啟一條線程,將耗時的操作放在新的線程中執(zhí)行
*/
- (void)threadTest{
NSLog(@"begin");
NSInteger count = 1000 * 100;
for (NSInteger i = 0; i < count; i++) {
// 棧區(qū)
NSInteger num = I;
// 常量區(qū)
NSString *name = @"zhang";
// 堆區(qū)
NSString *myName = [NSString stringWithFormat:@"%@ - %zd", name, num];
NSLog(@"%@", myName);
}
NSLog(@"over");
}
4.3 GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self threadTest];
});
4.4 NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
[self threadTest];
}];
