課程筆記:多線程相關(guān)面試問(wèn)題

AFNetwoking 和 SDWebImage 內(nèi)部都是用的 NSOperation

GCD

  • 同步/異步 和 串行/并行
  • dispatch_barrier_async
  • dispatch_group
同步/異步 和 串行/并行

分為:

  • dispatch_sync(serial_queuq, ^{//任務(wù)});
  • dispatch_async(serial_queuq, ^{//任務(wù)});
  • dispatch_sync(concurrentQueuq, ^{//任務(wù)});
  • dispatch_async(concurrentQueuq, ^{//任務(wù)});

串行

  • 同步串行

1、請(qǐng)思考下面這段代碼會(huì)發(fā)生什么?

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    dispatch_sync(dispatch_get_main_queue(), ^{
        
        [self dosomething];
        
    })
}

上面這段代碼會(huì)發(fā)生死鎖,死鎖的原因是:隊(duì)列引起的循環(huán)等待,并不是線程引起的循環(huán)等待

這段代碼的邏輯:在主隊(duì)列中提交了viewDidLoad任務(wù),然后提交 block任務(wù),這2個(gè)任務(wù)最終都需要分派的主線程中執(zhí)行。比如說(shuō)分派 viewDidLoad到主線程中處理,在執(zhí)行過(guò)程當(dāng)中,需要調(diào)用block,當(dāng)block同步調(diào)用完成后,viewDidLoad方法才能繼續(xù)向下執(zhí)行,所以viewDidLoad調(diào)用結(jié)束或者說(shuō)處理需要依賴于后續(xù)提交的block任務(wù)。主隊(duì)列的性質(zhì)是先進(jìn)先出,Block任務(wù)要執(zhí)行,依賴于 viewDidLoad任務(wù)完成。這個(gè)過(guò)程就造成死鎖。

2、請(qǐng)思考下面這段代碼會(huì)死鎖嗎?

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    dispatch_queue_t queue = dispatch_queue_create(DISPATCH_QUEUE_SERIAL, 0);
    dispatch_async(serialQueue, ^{
        [self dosomething];
    });
}

這段代碼沒(méi)有問(wèn)題,原因如下:

代碼邏輯:以上代碼涉及2個(gè)隊(duì)列,一個(gè)是主隊(duì)列,一個(gè)是串行隊(duì)列。
在viewDidLoad運(yùn)行在主線程中,viewDidLoad執(zhí)行到某一時(shí)刻時(shí),需要同步提交任務(wù)到對(duì)應(yīng)的串行隊(duì)列上。同步提交,意味著在當(dāng)前線程執(zhí)行,所以串行隊(duì)列提交的任務(wù),最終也是在主線程中執(zhí)行,串行隊(duì)列中提交的任務(wù)在主線程當(dāng)中執(zhí)行完成后,才去繼續(xù)執(zhí)行主隊(duì)列viewDidLoad后續(xù)的代碼邏輯.

  • 主線程和主隊(duì)列的關(guān)系:主隊(duì)列是主線程中的一個(gè)串行隊(duì)列,所有的和UI的操作(刷新或者點(diǎn)擊按鈕)都必須在主線程中的主隊(duì)列中去執(zhí)行,否則無(wú)法更新UI,每一個(gè)應(yīng)用程序只有唯一的一個(gè)主隊(duì)列用來(lái)更新 UI。

  • 隊(duì)列和線程的關(guān)系:在一個(gè)線程內(nèi)可能有多個(gè)隊(duì)列,這些隊(duì)列可能是串行的或者是并行的,按照同步或者異步的方式工作
    異步的,則會(huì)開啟新的線程工作
    同步的,會(huì)在當(dāng)前線程內(nèi)工作,不會(huì)創(chuàng)建新的線程
    注意:并行同步隊(duì)列,不會(huì)創(chuàng)建新的線程而且會(huì)是順序執(zhí)行相當(dāng)于串行同步隊(duì)列

  • 同步并發(fā)

1、思考下面這段代碼的輸出結(jié)果

-  (void)viewDidLoad
{
    NSLog(@"----1");
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_sync(queue, ^{
        NSLog(@"----2");
        
        dispatch_sync(queue, ^{
            NSLog(@"----3");
        });
        
        NSLog(@"-----4");
    });
    NSLog(@"-----5");
}

注意:只要是以同步的方式提交任務(wù),無(wú)論是串行隊(duì)列還是并行隊(duì)列都是在當(dāng)前線程執(zhí)行任務(wù)。
輸出結(jié)果是:

如果把上面代碼里的并行隊(duì)列換成串行隊(duì)列,將發(fā)生死鎖

  • 異步并發(fā)

1、請(qǐng)思考下面這段代碼的輸出結(jié)果?

-  (void)viewDidLoad
{
       dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
       dispatch_async(queue, ^{
        NSLog(@"---6");
        
        [self performSelector:@selector(printLog) withObject:nil afterDelay:0];
        
        NSLog(@"----8");
    });
}

- (void)printLog
{
    NSLog(@"7");
}

上面代碼的執(zhí)行結(jié)果是:6和8,7是不會(huì)打印的,因?yàn)?performSelector:withObject: afterDelay: 即使是延遲0秒,默認(rèn)也是需要開啟 RunLoop 的,而子線程中 RunLoop 默認(rèn)是沒(méi)有開啟的,因此這個(gè)方法在這里是會(huì)失效的。

dispatch_barrier_async()
  • 怎樣利用 GCD 實(shí)現(xiàn)多讀單寫?

具體實(shí)現(xiàn)如下:

#import "UserCenter.h"
@interface UserCenter()

@property (nonatomic, strong) NSMutableDictionary *userCenterDict;
@property (nonatomic, assign) dispatch_queue_t queue;

@end
@implementation UserCenter

- (instancetype)init
{
    self = [super init];
    if (self) {
        //創(chuàng)建并發(fā)隊(duì)列
        dispatch_queue_t queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        self.queue = queue;
        //用戶數(shù)據(jù)中心,可能多個(gè)線程需要數(shù)據(jù)訪問(wèn)
        self.userCenterDict = [NSMutableDictionary dictionary];
    }
    return self;
}

- (void)objectForKey:(NSString *)key
{
    __block id obj;
    //同步讀取指定數(shù)據(jù),這里用同步是因?yàn)橐罅⒖谭祷亟Y(jié)果,所以用同步
    dispatch_sync(self.queue, ^{
        obj = [self.userCenterDict objectForKey:key];
    });
}

- (void)setObject:(id)obj forKey:(NSString *)key
{
    //異步柵欄調(diào)用設(shè)置數(shù)據(jù)
    dispatch_barrier_async(self.queue, ^{
        [self.userCenterDict setObject:obj forKey:key];
    });
}

@end
dispatch_group_async()

思考如何使用GCD實(shí)現(xiàn)這個(gè)需求:A、B、C三個(gè)任務(wù)并發(fā),完成后執(zhí)行任務(wù)D?

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
  
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("group.create", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{
        //任務(wù)1;
    });
    dispatch_group_async(group, queue, ^{
        //任務(wù)2;
    });
    dispatch_group_async(group, queue, ^{
        //任務(wù)3;
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        //任務(wù)4;
    });
}
NSOperation

需要和 NSOperationQueue 配合使用來(lái)實(shí)現(xiàn)多線程方案

  1. 請(qǐng)思考使用 NSOperation 有哪些優(yōu)勢(shì)和特點(diǎn)?
  • 可以添加任務(wù)依賴,主要通過(guò) operation addDependency:operation removeDependency: 來(lái)實(shí)現(xiàn)
  • 任務(wù)執(zhí)行狀態(tài)控制
  • 可以控制最大并發(fā)量
任務(wù)執(zhí)行狀態(tài)控制
  • isReady 是否處于就緒狀態(tài)
  • isExecuting 當(dāng)前任務(wù)是否處于正在執(zhí)行中
  • isFinished 當(dāng)前任務(wù)是否完成
  • isCanceled 當(dāng)前任務(wù)是否已取消

狀態(tài)控制主要涉及 main 方法和 start 方法

  • 如果只重寫了 main 方法,底層控制變更任務(wù)執(zhí)行完成狀態(tài),以及任務(wù)退出
  • 如果重寫了 start 方法,自行控制任務(wù)狀態(tài)

下面參考 GNUstep 關(guān)于 NSOperation 的源來(lái)看下內(nèi)部實(shí)現(xiàn)機(jī)制

start.png
狀態(tài)
狀態(tài)2
image.png

Q:系統(tǒng)怎樣移除一個(gè) isFinished = YES 的NSOperation的?
A:通過(guò)KVO來(lái)移除 NSOperationQueue 里的 NSOperation

NSThread

通常是結(jié)合 RunLoop 來(lái)一塊考察的

  • NSThread 啟動(dòng)流程

其中里面的 main 函數(shù)是 NSThread 內(nèi)部的 main 函數(shù)
下面是 start 方法的內(nèi)部實(shí)現(xiàn)機(jī)制,同樣是基于 gnustep-base-1.24.9

start函數(shù)
image.png
image.png
image.png

NSThread的執(zhí)行原理是內(nèi)部創(chuàng)建了一個(gè) pThread 執(zhí)行線程,然后當(dāng) main 函數(shù)或者我們指定的target的selector方法執(zhí)行結(jié)束以后,會(huì)為我們執(zhí)行線程退出的管理操作,如果我們要想維護(hù)一個(gè)常駐線程的話,需要在 NSThread 對(duì)應(yīng)的 selector 方法中去維護(hù) runloop 的事件循環(huán)。

多線程的鎖
  • iOS當(dāng)中都有哪些鎖,或者你使用過(guò)哪些鎖?
    • @synchronized
    • atomic
    • OSSpinLock
    • NSRecursiveLock
    • NSLock
    • dispatch_semaphore_t

上面就構(gòu)成了我們經(jīng)常使用的鎖,這些鎖應(yīng)用在不同的場(chǎng)景下

@synchronized

一般在創(chuàng)建單例對(duì)象的時(shí)候使用

atomic
  1. 修改屬性的關(guān)鍵字
  2. 對(duì)被修飾的對(duì)象進(jìn)行原子操作(賦值操作保證安全,其他情況下不保證安全),示例如下:
OSSpinLock(自旋鎖)
  1. 循環(huán)等待詢問(wèn),不釋放當(dāng)前資源,類似一個(gè) while 操作,循環(huán)檢測(cè)是否能獲得鎖的訪問(wèn),如果不能,繼續(xù)輪詢,直到可以獲得
  2. 應(yīng)用場(chǎng)景:用于輕量級(jí)數(shù)據(jù)訪問(wèn),簡(jiǎn)單的int值+1/-1操作
NSLock

Q:以上代碼有什么問(wèn)題?
A:methodA, 加鎖后,methodB又對(duì)同一把鎖進(jìn)行加鎖,就相當(dāng)于已經(jīng)獲取到了鎖,又再次獲取這個(gè)鎖,就會(huì)由于重入的原因?qū)е滤梨i??梢允褂眠f歸鎖NSRecursiveLock來(lái)解決這個(gè)問(wèn)題,遞歸鎖的特點(diǎn)就是可以重入。

dispatch_semaphore_t
//根據(jù)一個(gè)初始值創(chuàng)建信號(hào)量
dispatch_semaphore_create(信號(hào)量值)
//如果信號(hào)量的值<=0,當(dāng)前線程就會(huì)進(jìn)入休眠等待(直到信號(hào)量的值>0);如果信號(hào)量的值>0,就減1,然后往下執(zhí)行后面的代碼。
dispatch_semaphore_wait(信號(hào)量,等待時(shí)間)
//提高信號(hào)量(讓信號(hào)量的值加1)
dispatch_semaphore_signal(信號(hào)量)

下面是 wait 方面的內(nèi)部處理邏輯,如果信號(hào)量值為0,則喚醒

下面是 signal 的內(nèi)部機(jī)制

常見面試題

我們使用GCD來(lái)實(shí)現(xiàn)一些簡(jiǎn)單的線程同步,包括一些子線程的分配,包括多讀單寫這些場(chǎng)景的解決,對(duì)于 NSOperation 由于它可以方便地讓我們對(duì)狀態(tài)進(jìn)行控制,添加依賴,移除依賴

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容