11--多線程01--多線程概念(線程和進程)

[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í)行任務
      • 交給報策略去處理
        1. Abort策略:默認策略,新任務提交時直接拋出未檢查的異常 RejectedExecution,該異??捎烧{用者捕獲
        2. CallerRuns策略:為調節(jié)機制,既不拋棄任務也不拋出異常,而是將某些任務會退到調用者。不會在線程池的線程中執(zhí)行新的任務,而是在調用exector的線程中運行新的任務
        3. Discard策略:新提交的任務被拋棄。
        4. 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 線程安全和線程通訊

  • atomicnonatomic的區(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ù):

    1. pthread_t:要創(chuàng)建線程的結構體指針,通常開發(fā)的時候,如果遇到 C 語言的結構體,類型后綴 _t / Ref 結尾
      同時不需要 *
    2. 線程的屬性,nil(空對象 - OC 使用的) / NULL(空地址,C 使用的)
    3. 線程要執(zhí)行的函數(shù)地址
      void *: 返回類型,表示指向任意對象的指針,和 OC 中的 id 類似
      (*): 函數(shù)名
      (void *): 參數(shù)類型,void *
    4. 傳遞給第三個參數(shù)(函數(shù))的參數(shù)
      • 返回值:C 語言框架中非常常見
        int
        0 創(chuàng)建線程成功!成功只有一種可能
        非 0 創(chuàng)建線程失敗的錯誤碼,失敗有多種可能!
        // 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;
        }
    
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];
}];
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容