iOS探索 多線程面試題分析

歡迎閱讀iOS探索系列(按序閱讀食用效果更加)程序員

寫在前面

前面四篇文章分別介紹了多線程原理GCD的應用GCD底層原理NSOperation,本文將分析iOS面試中高頻的多線程面試題,但願各位看官都能答對(部份內容跟前幾篇文章有點重複)面試

1、多線程的選擇方案

技術方案 簡介 語言 線程生命週期 使用評率
pthread 一套通用的多線程API
適用於Unix/Linux/Windows等系統
跨平臺/可移植
使用難度大
C 程序員管理 幾乎不用
NSThread 使用更加面向對象
簡單易用,可直接操做線程對象
OC 程序員管理 偶爾使用
GCD 旨在替代NSThread等線程技術
充分利用設備的多核
C 自動管理 常用
NSOperation 基於GCD(底層是GCD)
比GCD多了一些更簡單實用的功能
使用更加面向對象
OC 自動管理 常用

注意:若是使用NSThread的performSelector:withObject:afterDelay:時須要添加到當前線程的runloop中,由於在內部會建立一個NSTimer數組

2、GCD和NSOperation的比較

  • GCDNSOperation的關係以下:安全

    • GCD是面向底層的C語言的API
    • NSOperation是用GCD封裝構建的,是GCD的高級抽象
  • GCDNSOperation的對好比下:網絡

    1. GCD執行效率更高,並且因爲隊列中執行的是由block構成的任務,這是一個輕量級的數據結構——寫起來更加方便
    2. GCD只支持FIFO的隊列,而NSOpration能夠設置最大併發數、設置優先級、添加依賴關係等調整執行順序
    3. NSOpration甚至能夠跨隊列設置依賴關係,可是GCD只能經過設置串行隊列,或者在隊列內添加barrier任務才能控制執行順序,較爲複雜
    4. NSOperation支持KVO(面向對象)能夠檢測operation是否正在執行、是否結束、是否取消
  • 實際項目中,不少時候只會用到異步操做,不會有特別複雜的線程關係管理,因此蘋果推崇的是優化完善、運行快速的GCD
  • 若是考慮異步操做之間的事務性、順序性、依賴關係,好比多線程併發下載,GCD須要寫更多的代碼來實現,而NSOperation已經內建了這些支持
  • 無論是GCD仍是NSOperation,咱們接觸的都是任務和隊列,都沒有直接接觸到線程,事實上線程管理也的確不須要咱們操心,系統對於線程的建立、調度管理和釋放都作得很好;而NSThread須要咱們本身去管理線程的生命週期,還要考慮線程同步、加鎖問題,形成一些性能上的開銷

3、多線程的應用場景

  • 異步執行
    • 將耗時操做放在子線程中,使其不阻塞主線程
  • 刷新UI
    • 異步網絡請求,請求完畢dispatch_get_main_queue()回到主線程刷新UI
    • 同一頁面多個網絡請求使用dispatch_group統一調度刷新UI
  • dispatch_once
    • 單例中使用,一個類僅有一個實例且提供一個全局訪問點
    • method-Swizzling使用保證方法只交換一次
  • dispatch_after將任務延遲加入隊列
  • 柵欄函數可用做同步鎖
  • dispatch_semaphore_t
    • 用做鎖保證線程安全
    • 控制GCD的最大併發數
  • dispatch_source定時器替代偏差較大的NSTimer
  • AFNetworkingSDWebImage等知名三方庫中的NSOperation使用
  • ...

4、線程池的原理

  • 線程池大小小於核心線程池大小
    • 建立線程執行任務
  • 線程池大小大於等於核心線程池大小
    1. 先判斷線程池工做隊列是否已滿
    2. 若沒滿就將任務push進隊列
    3. 若已滿時,且maximumPoolSize>corePoolSize,將建立新的線程來執行任務
    4. 反之則交給飽和策略去處理
參數名 表明意義
corePoolSize 線程池的基本大小(核心線程池大小)
maximumPool 線程池的最大大小
keepAliveTime 線程池中超過corePoolSize樹木的空閒線程的最大存活時間
unit keepAliveTime參數的時間單位
workQueue 任務阻塞隊列
threadFactory 新建線程的工廠
handler 當提交的任務數超過maxmumPoolSize與workQueue之和時,
任務會交給RejectedExecutionHandler來處理

飽和策略有以下四個:數據結構

  • AbortPolicy直接拋出RejectedExecutionExeception異常來阻止系統正常運行
  • CallerRunsPolicy將任務回退到調用者
  • DisOldestPolicy丟掉等待最久的任務
  • DisCardPolicy直接丟棄任務

5、柵欄函數異同以及注意點

柵欄函數兩個API的異同多線程

  • dispatch_barrier_async:能夠控制隊列中任務的執行順序
  • dispatch_barrier_sync:不只阻塞了隊列的執行,也阻塞了線程的執行

柵欄函數注意點併發

  1. 儘可能使用自定義的併發隊列:
    • 使用全局隊列起不到柵欄函數的做用
    • 使用全局隊列時因爲對全局隊列形成堵塞,可能導致系統其餘調用全局隊列的地方也堵塞從而致使崩潰(並非只有你在使用這個隊列)
  2. 柵欄函數只能控制同一併發隊列:打個比方,平時在使用AFNetworking作網絡請求時爲何不能用柵欄函數起到同步鎖堵塞的效果,由於AFNetworking內部有本身的隊列

6、柵欄函數的讀寫鎖

多讀單寫功能指的是:能夠多個讀者同時讀取數據,而在讀的時候,不能寫入數據;在寫的過程當中不能有其餘寫者去寫。即讀者之間是併發的,寫者與其餘寫者、讀者之間是互斥的異步

- (id)readDataForKey:(NSString*)key {
    __block id result;
    dispatch_sync(_concurrentQueue, ^{
        result = [self valueForKey:key];
    });
    return result;
}

- (void)writeData:(id)data forKey:(NSString*)key {
    dispatch_barrier_async(_concurrentQueue, ^{
        [self setValue:data forKey:key];
    });
}
複製代碼
  • 讀:併發同步獲取到值後返回給讀者
    • 若使用併發異步則會先返回空的result 0x0,再經過getter方法獲取到值
  • 寫:寫的那個時間段,不能有任何讀者+其餘寫者
    • dispatch_barrier_async知足:等隊列中前面的讀寫任務都執行完了再來執行當前任務

7、GCD的併發量

不一樣於NSOperation中能夠經過maxConcurrentOperationCount去控制併發數,GCD須要經過信號量才能達到效果async

dispatch_semaphore_t sem = dispatch_semaphore_create(1);
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);

for (int i = 0; i < 10; i++) {
    dispatch_async(queue, ^{
        NSLog(@"當前%d----線程%@", i, [NSThread currentThread]);
        // 打印任務結束後信號量解鎖
        dispatch_semaphore_signal(sem);
    });
    // 因爲異步執行,打印任務會較慢,因此這裏信號量加鎖
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}

--------------------輸出結果:-------------------
當前1----線程<NSThread: 0x600001448d40>{number = 3, name = (null)}
當前0----線程<NSThread: 0x60000140c240>{number = 6, name = (null)}
當前2----線程<NSThread: 0x600001448d40>{number = 3, name = (null)}
當前3----線程<NSThread: 0x60000140c240>{number = 6, name = (null)}
當前4----線程<NSThread: 0x60000140c240>{number = 6, name = (null)}
當前5----線程<NSThread: 0x600001448d40>{number = 3, name = (null)}
當前6----線程<NSThread: 0x600001448d40>{number = 3, name = (null)}
當前7----線程<NSThread: 0x60000140c240>{number = 6, name = (null)}
當前8----線程<NSThread: 0x600001448d40>{number = 3, name = (null)}
當前9----線程<NSThread: 0x60000140c240>{number = 6, name = (null)}
--------------------輸出結果:-------------------
複製代碼

在面試中更多會考驗開發人員對於指定場景的多線程知識,接下來就來看看一些綜合運用

8、綜合運用一

1.下列代碼會報錯嗎?

int a = 0;
while (a < 5) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        a++; 
    });
}
複製代碼
  • 編譯會報錯Variable is not assignable (missing __block type specifier)
    • 這塊屬於block的知識
  • 捕獲外界變量並進行修改須要加__block int a = 0;
    • 這塊內容在接下來的block會講到

2.下列代碼的輸出

__block int a = 0;
while (a < 5) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        a++;
    });
}
NSLog(@"%d", a);
複製代碼
  • 會輸出0嗎?
    • 不會,儘管是併發異步執行,可是有while在,不知足條件就不會跳出循環
  • 會輸出1~4嗎?
    • 不會(緣由請往下看)
  • 會輸出5嗎?
    • 有可能(緣由請往下看)
  • 會輸出6~∞嗎?
    • 極有可能

分析:

  • 剛進入while循環時,a=0,而後進行a++
  • 因爲是異步併發會開闢子線程並有可能超車完成
    • 線程2a=0執行a++時,線程3有可能已經完成了a++使a=1
    • 因爲是操做同一片內存空間,線程3修改了a致使線程2a的值也發生了變化
    • 慢一拍的線程2對已是a=1進行a++操做
  • 同理還有線程4線程5線程n的存在
    • 能夠這麼理解,線程二、三、四、五、6同時在a=0時操做a
    • 線程二、三、四、5按順序完成了操做,此時a=4
    • 而後線程6開始操做了,可是它還沒執行完就跳到了下一次循環了開闢了線程7開始a++
    • 線程6執行結束脩改a=5以後來到while條件判斷就會跳出循環
    • 然而I/O輸出比較耗時,此時線程7又恰好完成了再打印,就會輸出大於5
  • 也有那麼種理想狀況,異步併發都比較聽話,恰好在a=5時沒有子線程
    • 此時就會輸出5

若是尚未明白能夠在while循環中添加打印代碼

__block int a = 0;
while (a < 5) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"%d————%@", a, [NSThread currentThread]);
        a++;
    });
}
NSLog(@"此時的%d", a);
複製代碼

打印信息證實while外面的打印已經執行,可是子線程仍是有可能在對a進行操做的

3.怎麼解決線程不安全?

可能有的小夥伴說這種需求不存在,可是咱們只管解決即是了

此時咱們應該能想到一下幾種解決方案:

  • 同步函數替換異步函數
  • 使用柵欄函數
  • 使用信號量
  1. 同步函數替換異步函數
  • 結果:能知足需求
  • 效果:不是很好——能使用異步函數去使喚子線程爲何不用呢(雖然會消耗內存,可是效率高)
  1. 使用柵欄函數
  • 結果:能知足需求
  • 效果:通常
    • 首先柵欄函數全局隊列搭配使用會無效,須要更換隊列類型;
    • 其次dispatch_barrier_sync會阻塞線程,影響性能
    • dispatch_barrier_async不能知足需求,它只能控制前面的任務執行完畢再執行柵欄任務(控制任務執行)但是異步柵欄執行也是在子線程中,當a=4時會先繼續下一次循環添加任務到隊列中,再來異步執行柵欄任務(不能控制任務的添加)
__block int a = 0;
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
while (a < 5) {
    dispatch_async(queue, ^{
        a++;
    });
    dispatch_barrier_async(queue, ^{});
}

NSLog(@"此時的%d", a);
sleep(1);
NSLog(@"此時的%d", a);

--------------------輸出結果:-------------------
此時的5
此時的17
--------------------輸出結果:-------------------
複製代碼
  1. 使用信號量
  • 結果:能知足需求
  • 效果:很好、簡潔效率高
__block int a = 0;
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
while (a < 5) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        a++;
        dispatch_semaphore_signal(sem);
    });
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}

NSLog(@"此時的%d", a);
sleep(1);
NSLog(@"此時的%d", a);

--------------------輸出結果:-------------------
此時的5
此時的5
--------------------輸出結果:-------------------
複製代碼

9、綜合運用二

1.輸出內容

dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
NSMutableArray *marr = @[].mutableCopy;
for (int i = 0; i < 1000; i++) {
    dispatch_async(queue, ^{
        [marr addObject:@(i)];
    });
}
NSLog(@"%lu", marr.count);
複製代碼
  • 你:輸出一個小於1000的數,由於for循環中是異步操做
  • 面試官:回去等消息吧
  • 而後你回去以後試了下大吃一驚——程序崩了

這是爲何呢?

其實跟綜合運用一是同樣的道理——for循環異步時無數條線程訪問數組,形成了線程不安全

2.怎麼解決線程不安全?

  • 使用串行隊列
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_SERIAL);
NSMutableArray *marr = @[].mutableCopy;
for (int i = 0; i < 1000; i++) {
    dispatch_async(queue, ^{
        [marr addObject:@(i)];
    });
}
NSLog(@"%lu", marr.count);

--------------------輸出結果:-------------------
998
--------------------輸出結果:-------------------
複製代碼
  • 使用互斥鎖
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
NSMutableArray *marr = @[].mutableCopy;
for (int i = 0; i < 1000; i++) {
    dispatch_async(queue, ^{
        @synchronized (self) {
            [marr addObject:@(i)];
        }
    });
}
NSLog(@"%lu", marr.count);

--------------------輸出結果:-------------------
997
--------------------輸出結果:-------------------
複製代碼
  • 使用柵欄函數
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
NSMutableArray *marr = @[].mutableCopy;
for (int i = 0; i < 1000; i++) {
    dispatch_async(queue, ^{
        [marr addObject:@(i)];
    });
    dispatch_barrier_async(queue, ^{});
}
NSLog(@"%lu", marr.count);
複製代碼

3.分析思路

單路千萬條,跳跳通羅馬——固然除了這三種還有其餘辦法

  • 使用串行隊列
    • 雖然效率低,但總歸能解決線程安全問題
    • 雖然串行異步是任務一個接一個執行,但那是隊列中的任務才知足執行規律
    • 要想獲得打印結果1000,能夠在隊列中執行
    • 總的來講,能知足需求但不是頗有效
  • 使用互斥鎖
    • @synchronized是個好東西,簡單易用還有效,但也沒有知足咱們的需求
    • 在for循環外使用隊列內同步/異步都不能獲得100
    • 要麼先sleep一秒——這樣不可控的代碼是不可取的的
    • 且在iOS的鎖家族中@synchronized效率很低
  • 使用柵欄函數
    • 柵欄函數能夠有效的控制任務的執行
    • 且與綜合運用一不一樣,本題中是for循環
    • 至於怎麼獲得打印結果1000,只須要在同一隊列中打印便可(柵欄函數的注意點)
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
NSMutableArray *marr = @[].mutableCopy;
for (int i = 0; i < 1000; i++) {
    dispatch_async(queue, ^{
        [marr addObject:@(i)];
    });
    dispatch_barrier_async(queue, ^{});
}
dispatch_async(queue, ^{
    NSLog(@"%lu", marr.count);
});
複製代碼

寫在後面

多線程在平常開發中佔有很多分量,同時面試中也是必問模塊。但只有基礎知識是一成不變的,綜合運用題稍有改動就是另一種類型的知識考量了,並且也有多種解決方案

相關文章
相關標籤/搜索