iOS底層學習 - 多線程之基礎原理篇

多線程是iOS開發中的一個重要的環節,不論是在平常的開發,仍是面試中,都有着舉足輕重的地位,因此打算開闢一個小專題,研究多線程相關的底層原理。這一篇章是第一篇,介紹一些基礎的概念性的多線程。前端

進程

定義

當一個程序進入內存運行時,即變成一個進程。進程是處於運行過程當中的程序,而且具備必定的獨立功能,進程是系統進行資源分配和調度的一個獨立單位。每一個進程之間是獨立的,每一個進程均運行在其專用的且受保護的內存。程序員

iOS開發中,一個App在內存中就是一個進程,且相互獨立,只能訪問本身的沙盒控件,這也是蘋果運行可以流暢安全的一個主要緣由。面試

特色

  • 獨立性:是系統獨立存在的實體,擁有本身獨立的資源,有本身私有的地址空間。在沒有通過進程自己容許的狀況下,一個用戶的進程不能夠直接訪問其餘進程的地址空間。
  • 動態性:進程與程序的區別在於:程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集和,進程中加入了時間的概念。進程具備本身的生命週期和不一樣的狀態,這些都是程序不具有的。
  • 併發性:多個進程能夠在單個處理器上併發執行,多個進程之間不會相互影響。

線程

定義

線程的定義,主要能夠歸結爲如下3點:後端

  1. 線程是進程的基本執行單元,一個進程的全部任務都在線程中執行
  2. 進程要想執行任務,必須得有線程,進程至少要有一條線程
  3. 程序啓動會默認開啓一條線程,這條線程被稱爲主線程或 UI 線程

進程與線程的關係

  • 地址空間:同一進程的線程共享本進程的地址空間,而進程之間則是獨立的地址空間。數組

  • 資源擁有:同一進程內的線程共享本進程的資源如內存、I/O、cpu等,可是進程之間 的資源是獨立的。安全

  • 一個進程崩潰後,在保護模式下不會對其餘進程產生影響,可是一個線程崩潰整個進程 都死掉。因此多進程要比多線程健壯。多線程

  • 進程切換時,消耗的資源大,效率高。因此涉及到頻繁的切換時,使用線程要好於進程。 一樣若是要求同時進行而且又要共享某些變量的併發操做,只能用線程不能用進程併發

  • 執行過程:每一個獨立的進程有一個程序運行的入口、順序執行序列和程序入口。可是線 程不能獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。異步

  • 線程是處理器調度的基本單位,可是進程不是。socket

線程與Runloop的關係

  1. runloop與線程是一一對應的,一個runloop對應一個核心的線程,爲何說是核心的, 是由於runloop是能夠嵌套的,可是核心的只能有一個,他們的關係保存在一個全局的字典裏。
  2. runloop是來管理線程的,當線程的runloop被開啓後,線程會在執行完任務後進入休眠狀態,有了任務就會被喚醒去執行任務。
  3. runloop在第一次獲取時被建立,在線程結束時被銷燬。
  4. 對於主線程來講,runloop在程序一啓動就默認建立好了。
  5. 對於子線程來講,runloop是懶加載的,只有當咱們使用的時候纔會建立,因此在子線程用定時器要注意:確保子線程的runloop被建立,否則定時器不會回調

隊列

定義

隊列,又稱爲佇列(queue),是先進先出FIFO, First-In-First-Out)的線性表,在具體應用中一般用鏈表或者數組來實現。裝載線程任務的隊形結構。隊列只容許在後端(稱爲rear)進行插入操做,在前端(稱爲front)進行刪除操做。隊列的操做方式和堆棧相似,惟一的區別在於隊列只容許新數據在後端進行添加。

類型

隊列的類型決定了任務的執行方式(併發、串行),隊列包括如下幾種:

  • 併發隊列(Concurrent Dispatch Queue): 線程執行能夠同時一塊兒進行執行,不須要上一個執行完,就能執行下一個的。
  • 串行隊列(Serial Dispatch Queue): 線程執行只能依次逐一前後有序的執行,等待上一個執行完,再執行下一個。
  • 主隊列:綁定主線程,全部任務都在主線程中執行,有通過特殊處理的串行隊列。
  • 全局隊列:系統提供的併發隊列。

同步、異步

同步 sync: 只能在當前線程按前後順序依次執行任務,不具有開啓新線程的能力。

異步 async: 在新的線程中執行任務,具有開啓新線程的能力。

多線程

概念和原理

一個進程中能夠併發多個線程同時執行各自的任務,叫作多線程。

分時操做系統會把CPU的時間劃分爲長短基本相同的時間區間,叫時間片,在一個時間片內,CPU只能處理一個線程中的一個任務,對於一個單核CPU來講,在不一樣的時間片來執行不一樣線程中的任務,就造成了多個任務在同時執行的「假象」。

多線程即爲單位時間片裏快速在各個線程之間切換

生命週期

  • 新建(New): 新建線程
  • 就緒(Runnable) : start以後就會runnable,而後等待cpu分配資源,進行調用
  • 運行(Running) : 當得到cpu調度以後就會到running狀態
  • 阻塞(Block) : 當線程中任務異常是,好比sleep,或者死鎖等操做以後就會形成線程阻塞.當阻塞解除以後不會直接到running狀態而是又到就緒狀態
  • 死亡(Dead) : 任務執行完成或者強制退出

優缺點

優勢:

  1. 能適當提升程序的執行效率
  2. 能適當提升資源利用率(cpu,內存)
  3. 線程上的任務執行完成後,線程會自動銷燬

缺點:

  1. 開啓線程須要佔用必定的內存空間(默認狀況下,每一個線程都佔512KB)
  2. 若是開啓大量的線程,會佔用大量的內存空間,下降程序性能
  3. 線程越多,CPU在調用線程上的開銷越大
  4. 程序設計更加複雜,好比線程間的通信,多線程的數據共享

iOS中的多線程方案

  1. pthread:即POSIX Thread,縮寫稱爲pthread,是線程的POSIX標準,是一套通用的多線程API,能夠在Unix/Linux/Windows等平臺跨平臺使用。iOS中基本不使用。
  2. NSThread:蘋果封裝的面向對象的線程類,能夠直接操做線程,比起GCDNSThread效率更高,由程序員自行建立,當線程中的任務執行完畢後,線程會自動退出,程序員也可手動管理線程的生命週期。使用頻率較低。
  3. GCD:全稱Grand Central Dispatch,由C語言實現,是蘋果爲多核的並行運算提出的解決方案,CGD會自動利用更多的CPU內核,自動管理線程的生命週期,程序員只須要告訴GCD須要執行的任務,無需編寫任何管理線程的代碼。GCD也是iOS使用頻率最高的多線程技術。
  4. NSOperation:基於GCD封裝的面向對象的多線程技術,常配合NSOperationQueue使用,使用頻率較高。

GCD和NSOperation區別

  1. GCD僅僅支持FIFO隊列,不支持異步操做之間的依賴關係設置。而NSOperation中的隊列能夠被從新設置優先級,從而實現不一樣操做的執行順序調整。
  2. NSOperation支持KVO,能夠觀察任務的執行狀態。
  3. GCD更接近底層,GCD在追求性能的底層操做來講,是速度最快的。
  4. 從異步操做之間的事務性,順序行,依賴關係。GCD須要本身寫更多的代碼來實現,而NSOperation已經內建了這些支持
  5. 若是異步操做的過程須要更多的被交互和UI呈現出來,NSOperation更好。底層代碼中,任務之間不太互相依賴,而須要更高的併發能力,GCD則更有優點

小結圖表

線程池

定義

線程池是多線程處理的一種形式,處理過程當中將任務添加到隊列,而後在建立線程後自動啓動這些任務。線程池中的線程都是後臺線程。每一個線程都有默認的堆棧大小,以默認的優先級運行,並處在多線程單元中。

執行流程

  1. 判斷線程池大小是否小核心線程池大小
  2. 若是小於.建立線程執行任務
  3. 若是不小於 判斷工做隊列是否已滿,不滿,將任務提交到工做隊列,建立線程執行任務,若是已滿,判斷線程是否都在工做,若是都在工做交給飽和策略,若是沒滿建立線程執行任務

飽和策略

  1. AbortPolicy:默認策略。直接拋出RejectedExecutionExeception異常阻止系統正常運行,該異常由調用者捕獲
  2. CallerRunsPolicy:調節機制。既不拋棄也不報異常。將任務回退給調用者
  3. DisOldestPolicy:丟掉等待最久的任務
  4. DisCardPolicy:直接丟棄任務

線程間通信

蘋果的官方文檔中,給出了幾種線程間通訊的方式:

  • 直接消息傳遞: 經過 performSelector 的一系列方法,能夠實現由某一線程指定在另外的線程上執行任務。由於任務的執行上下文是目標線程,這種方式發送的消息將會自動的被序列化。
  • 全局變量、共享內存塊和對象: 在兩個線程之間傳遞信息的另外一種簡單方法是使用全局變量,共享對象或共享內存塊。儘管共享變量既快速又簡單,可是它們比直接消息傳遞更脆弱。必須使用鎖或其餘同步機制仔細保護共享變量,以確保代碼的正確性。 不然可能會致使競爭情況,數據損壞或崩潰。
  • 條件執行: 條件是一種同步工具,可用於控制線程什麼時候執行代碼的特定部分。您能夠將條件視爲關守,讓線程僅在知足指定條件時運行。
  • Runloop sources: 一個自定義的 Runloop source 配置可讓一個線程上收到特定的應用程序消息。因爲 Runloop source 是事件驅動的,所以在無事可作時,線程會自動進入睡眠狀態,從而提升了線程的效率。
  • Ports and sockets: 基於端口的通訊是在兩個線程之間進行通訊的一種更爲複雜的方法,但它也是一種很是可靠的技術。更重要的是,端口和套接字可用於與外部實體(例如其餘進程和服務)進行通訊。爲了提升效率,使用 Runloop source 來實現端口,所以當端口上沒有數據等待時,線程將進入睡眠狀態。
  • 消息隊列: 傳統的多處理服務定義了先進先出(FIFO)隊列抽象,用於管理傳入和傳出數據。儘管消息隊列既簡單又方便,可是它們不如其餘一些通訊技術高效。
  • Cocoa 分佈式對象: 分佈式對象是一種 Cocoa 技術,可提供基於端口的通訊的高級實現。儘管能夠將這種技術用於線程間通訊,可是強烈建議不要這樣作,由於它會產生大量開銷。分佈式對象更適合與其餘進程進行通訊,儘管在這些進程之間進行事務的開銷也很高

端口通訊例子

對於performSelector的通訊,平時在開發中運用的比較多,就不詳細講述了,咱們講一個線程以前,運用端口來傳遞消息的例子來加深印象。

首先咱們建立一個類來發送消息:

@interface WYPerson : NSObject
- (void)personLaunchThreadWithPort:(NSPort *)port;
@end
複製代碼
#import "WYPerson.h"

@interface WYPerson()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *vcPort;
@property (nonatomic, strong) NSPort *myPort;
@end

@implementation WYPerson


- (void)personLaunchThreadWithPort:(NSPort *)port{
    
    NSLog(@"VC 響應了Person裏面");
    @autoreleasepool {
        ✅//1. 保存主線程傳入的port
        self.vcPort = port;
        ✅//2. 設置子線程名字
        [[NSThread currentThread] setName:@"WYPersonThread"];
        ❗️//3. 開啓runloop(重點在這)
        [[NSRunLoop currentRunLoop] run];
        ✅//4. 建立本身port
        self.myPort = [NSMachPort port];
        ✅//5. 設置port的代理回調對象(NSMachPortDelegate)
        self.myPort.delegate = self;
        ✅//6. 完成向主線程port發送消息
        [self sendPortMessage];
    }
}


/** * 完成向主線程發送port消息 */

- (void)sendPortMessage {
 
    NSData *data1 = [@"WY" dataUsingEncoding:NSUTF8StringEncoding];

    NSMutableArray *array  =[[NSMutableArray alloc]initWithArray:@[data1,self.myPort]];
    ✅// 發送消息到VC的主線程// 第一個參數:發送時間。// msgid 消息標識。// components,發送消息附帶參數。// reserved:爲頭部預留的字節數
    [self.vcPort sendBeforeDate:[NSDate date]
                          msgid:10086
                     components:array
                           from:self.myPort
                       reserved:0];
    
}

#pragma mark - NSMachPortDelegate 處理端口傳遞信息

- (void)handlePortMessage:(NSPortMessage *)message{
    NSLog(@"person:handlePortMessage == %@",[NSThread currentThread]);
    NSLog(@"從VC 傳過來一些信息:");
    NSLog(@"components == %@",[message valueForKey:@"components"]);
    NSLog(@"receivePort == %@",[message valueForKey:@"receivePort"]);
    NSLog(@"sendPort == %@",[message valueForKey:@"sendPort"]);
    NSLog(@"msgid == %@",[message valueForKey:@"msgid"]);
}


複製代碼

接着咱們建立PortViewController用來接收消息和回調消息給發送者

#import "PortViewController.h"
#import <objc/runtime.h>
#import "WYPerson.h"

@interface PortViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) WYPerson *person;

@end

@implementation PortViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"Port線程通信";
    self.view.backgroundColor = [UIColor whiteColor];

    ✅//1. 建立主線程的port// 子線程經過此端口發送消息給主線程
    self.myPort = [NSMachPort port];
    ✅//2. 設置port的代理回調對象
    self.myPort.delegate = self;
    ❗️//3. 把port加入runloop,接收port消息(此時主線程已經再跑,不須要run)
    [[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    self.person = [[WYPerson alloc] init];
    [NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
                             toTarget:self.person
                           withObject:self.myPort];
    
}

#pragma mark - NSMachPortDelegate

- (void)handlePortMessage:(NSPortMessage *)message{
    
    NSLog(@"VC == %@",[NSThread currentThread]);
    
    NSLog(@"從person 傳過來一些信息:");
    //會報錯,沒有這個隱藏屬性
    //NSLog(@"from == %@",[message valueForKey:@"from"]);
    
    NSArray *messageArr = [message valueForKey:@"components"];
    NSString *dataStr   = [[NSString alloc] initWithData:messageArr.firstObject  encoding:NSUTF8StringEncoding];
    NSLog(@"傳過來一些信息 :%@",dataStr);
    NSPort  *destinPort = [message valueForKey:@"remotePort"];
    
    if(!destinPort || ![destinPort isKindOfClass:[NSPort class]]){
        NSLog(@"傳過來的數據有誤");
        return;
    }
    
    NSData *data = [@"VC收到!!!" dataUsingEncoding:NSUTF8StringEncoding];
    
    NSMutableArray *array  =[[NSMutableArray alloc]initWithArray:@[data,self.myPort]];
    
    ❗️❗️❗️// 很是重要,若是你想在Person的port接受信息,必須加入到當前主線程的runloop
    [[NSRunLoop currentRunLoop] addPort:destinPort forMode:NSDefaultRunLoopMode];
    
    NSLog(@"VC == %@",[NSThread currentThread]);
    
    BOOL success = [destinPort sendBeforeDate:[NSDate date]
                                        msgid:10010
                                   components:array
                                         from:self.myPort
                                     reserved:0];
    NSLog(@"%d",success);

}


- (void)getAllProperties:(id)somebody{
    
    u_int count = 0;
    objc_property_t *properties = class_copyPropertyList([somebody class], &count);
    for (int i = 0; i < count; i++) {
        const char *propertyName = property_getName(properties[i]);
         NSLog(@"%@",[NSString stringWithUTF8String:propertyName]);
    }
}

複製代碼

打印結果以下:

經過上面的例子,咱們已經實現了線程之間的通訊

稍微總結一下NSPort的使用要點:

  1. NSPort對象必須添加到要接收消息的線程的Runloop中,必須由Runloop來進行管理
  2. 接收消息的對象實現NSPortDelegate協議的-handlePortMessage:方法來獲取消息內容

參考資料

iOS底層原理探索—多線程的本質

iOS 查漏補缺 - 線程

相關文章
相關標籤/搜索