iOS底層探索 -- 多線程

1. 多線程概念問題

  • 線程的定義

    • 線程是進程的基本單元,一個進程的所有任務都在線程中執行
    • 進程要想執行任務,必須得有線程,進程至少有有一條線程
    • 程序啓動默認會開啓一條線程,這條線程被稱爲主線程或者UI線程
  • 進程的定義

    • 進程是指在系統中正在運行的一個應用程序
    • 每個進程之間是互相獨立的,每個進程均運行在其專用的且受保護的內存中
  • 進程和線程的關係

    • 地址空間:同一進程的線程共享本進程的地址空間,而進程之間相互獨立

    • 資源擁有:同一進程的線程共享本進程的資源(如:內存、CPU),進程之間是相互獨立的

    • 一個進程崩潰會,在保護模式下不會對其他進程產生影響,但是一個線程崩潰,整個進程都死掉,所以,多進程要不多線程健壯(iOS沒有多進程)

    • 進程切換是,消耗的資源大,效率高,所以涉及頻繁切換進程時,使用線程要優於進程。同樣如果要求同時進行並且又要共享某些變量的併發造作,只能用線程

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

    • 線程是處理器調度的基本單元,進程不是

  • 多線程的意義

    • 能適當提高程序的執行效率
    • 能適當提高資源的利用率(CPU,內存)
    • 線程上的任務執行完成後,線程會自動銷燬
    • 開啓線程需要佔用一定的內存空間(默認 每個線程都佔 512K )
    • 如果開啓大量的線程,會佔用大量的內存
    • 程序設計更加複雜,比如線程之間的通訊,多線程共享數據
  • 線程和Runloop的關係

    • runloop與線程是一一對應的,一個runloop對應一個核心的線程(runloop是可以嵌套的,但是核心的線程只能有一個,他們的關係保存在一個字典裏)
    • runloop是管理線程的,當線程的runloop被開啓後,線程會在執行完任務後進入休眠狀態,有任務會被喚醒去執行任務
    • runloop在第一次獲取是被常見,在線程結束是銷燬
    • 對主線程來說,runloop在程序一啓動就默認創建好
    • 對於子線程來說,runloop是懶加載的,只有當我們使用的時候纔會創建,所以在子線程用定時器要注意,確保子線程的runloop被創建,不然定時器不會回調。
  • atomicnonatomic 的區別

    • nonatomic非原子性,非線程安全,適合內存小的移動設備

    • atomic 原子屬性(線程安全,需要消耗大量的資源),針對多線程設計的,默認值

      保證同一時間只有一個線程能夠寫入(可以同一時間多個線程取值)
      本身是一把自旋鎖

    • 建議使用nonatomic聲明屬性,儘量避免多線程搶奪同一資源

    • 儘量將加鎖,資源搶奪的業務邏輯交給服務器處理,減少客戶端壓力

  • COC 的橋接

    • __bridge只做類型轉換,不修改對象(內存)管理權
    • __bridge_retainedOC對象轉換爲Core Foundation對象,同時將對象(內存)的管理權交給我們,後續需要 使用CFRelease或者相關方法來釋放對象
    • __bridge_transferCore Foundation的對象 轉換爲Objective-C的對象,同時將對象(內存)的管理權交給ARC

2. 多線程原理

  • 多線程原理

    • CUP在單位時間片裏快速在各個線程之前切換
    • 多核是爲了加快執行的效率
  • 多線程生命週期

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-e4PZRCdP-1588931804943)(https://user-gold-cdn.xitu.io/2020/5/8/171f23ec24188bc2?w=1167&h=394&f=png&s=70827)]

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FaQyPWJ4-1588931804964)(https://user-gold-cdn.xitu.io/2020/5/8/171f2472ba57bdeb?w=1137&h=518&f=png&s=82193)]

飽和策略:

  • AbortPolicy 直接拋出RejectedExecutionExeception 異常來阻止系統正常運行
  • CallerRunsPolicy 將任務回退到調用者
  • DisOldestPolicy 丟掉等待最久的任務
  • DisCardPolicy 直接丟棄任務
  • 這四種拒絕策略均實現的RejectedExecutionHandler接口

3. 端口通訊

VC

    //1. 創建主線程的port
    // 子線程通過此端口發送消息給主線程
    self.myPort = [NSMachPort port];
    //2. 設置port的代理回調對象
    self.myPort.delegate = self;
    //3. 把port加入runloop,接收port消息
    [[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    self.person = [[KCPerson alloc] init];
    [NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
                             toTarget:self.person
                           withObject:self.myPort];
                           
    
- (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);

}

Person

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


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

- (void)sendPortMessage {
 
    NSData *data1 = [@"Gavin" 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"]);
    
    NSArray *messageArr = [message valueForKey:@"components"];
    NSString *dataStr   = [[NSString alloc] initWithData:messageArr.firstObject  encoding:NSUTF8StringEncoding];
    NSLog(@"傳過來一些信息 :%@",dataStr);
}

解析:

1. 在 VC 中創建主線程的port(子線程通過此端口發消息給主線程),並設置 port  的代理回調對象
2. 將 port 加入到 runloop,接收 port 消息。
3. 實現 handlePortMessage 代理方法

4. GCD 初探

GCD 是一套純 C 語言 API,提供了非常多的強大的函數,自動管理線程的生命週期(創建線程,調度任務,銷燬線程)。只需要將任務添加到隊列,並且指定執行任務的函數

任務是由block封裝的(無參數,無返回值)

執行任務的函數有兩種:

  • 異步 dispatch_async,不用等待當前語句執行完畢,就可以執行下一條語句,會開闢新的線程執行任務
  • 同步 dispatch_sync,必須等待當前語句執行完畢,纔會執行下一條語句,不會開闢新的線程,在當前線程執行block任務

隊列分爲兩種:

  • 串行隊列

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fdzsliej-1588931804967)(https://user-gold-cdn.xitu.io/2020/5/8/171f325d8797889b?w=627&h=222&f=png&s=32586)]

  • 併發隊列

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-96uuEc6I-1588931804979)(https://user-gold-cdn.xitu.io/2020/5/8/171f3260da4cf0a3?w=742&h=291&f=png&s=38456)]

那麼還有下面兩種隊列:主隊列和全局隊列

  • 主隊列dispatch_get_main_queue()

    • 專門用來在住線程上調度任務的隊列,不會開啓線程
    • 如果當前住線程正在有任務執行,那麼無論主隊列中當前添加了什麼任務都不會被調度
  • 全局隊列dispatch_get_global_queue()

    • 是一個併發隊列
    • 在使用多線程開發是,如果隊列沒有特殊需求,在執行異步任務時,可以直接使用全局隊列

隊列和函數組合有以下四種情況:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-q6RsjtHz-1588931804981)(https://user-gold-cdn.xitu.io/2020/5/8/171f32c21bc01ac7?w=1030&h=513&f=png&s=76597)]

接下來,看一下GCD的經典面試題

- (void)textDemo{
    
    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
    // 1 5 2 4 3
}

上面代碼打印結果是什麼?

其實上面的代碼很簡單,打印結果是1 5 2 4 3

分析:
首先是打印 1 ,然後是併發隊列加異步函數,這個操作是異步耗時,所以先執行打印 5,
然後是耗時操作,打印 2 ,接着又是併發隊列加異步函數,同理先打印 4, 在打印3。

接着看下面的題目

dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);

    /***
     1 2 3
     0
     7 8 9
     */
    
    dispatch_async(queue, ^{
        // sleep(2);
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    // 堵塞 - 護犢子
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    // **********************
    NSLog(@"0");

    dispatch_async(queue, ^{
        NSLog(@"7");
    });
    dispatch_async(queue, ^{
        NSLog(@"8");
    });
    dispatch_async(queue, ^{
        NSLog(@"9");
    });
    // A: 1230789
    // B: 1237890
    // C: 3120798
    // D: 2137890

上面的結果是A C

分析:
首先是併發隊列,然後是兩個異步耗時操作,緊接着是一個同步函數,這樣會堵塞後面的任務
的執行,再接着是兩個異步操作。

所以,0 是在中間的,前面 1、2、3和後面7、8、9,順序未知(依賴於任務複雜度和cpu的調度)

接着看下面的題目

下面的代碼打印什麼?

    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");

上面的結果是1 5 2,然後死鎖崩潰

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9fAnpBP5-1588931804983)(https://user-gold-cdn.xitu.io/2020/5/8/171f3abb629de0e6?w=603&h=316&f=png&s=28382)]

分析:
首先是串行隊列,先打印1,然後是異步併發,耗時操作, 先打印 5,然後將打
印 2 的任務加到隊列,然後將代碼塊加入隊列,再將打印4 的任務加入隊列,然後將同步
打印3 的任務加入隊列,

而任務 4 的執行,必須在代碼塊執行完之後,而代碼塊是同步,必須等任務3執行完,
而任務 3,在等待任務 4 執行完,就造成了相互等待,死鎖的問題

同理,將打印 4,放到同步函數前,一樣會死鎖

    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    // 異步函數
    dispatch_async(queue, ^{
        NSLog(@"2");
        NSLog(@"4");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
    });
    NSLog(@"5");

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章