《iOS面試題整理》- GCD、多線程相關(guān)面試題

基本概念

進程和線程的區(qū)別

  • 進程是指系統(tǒng)中正在運行的一個應(yīng)用程序, 每個進程之間是相互獨立的
  • 一個進程中可以有多條線程, 進程的所有任務(wù)都在線程中執(zhí)行的

進程的狀態(tài)

  • 新建
  • 就緒 : 線程對象加入線程池中等待 CPU 調(diào)度
  • 運行 : CPU負責(zé)調(diào)度線程中線程的執(zhí)行, 線程執(zhí)行完成前, 狀態(tài)可能在就緒和運行之間來回切換
  • 阻塞 : 滿足某個預(yù)定條件, 使用休眠或鎖, 阻塞線程執(zhí)行
  • 死亡 : 線程執(zhí)行完畢, 或者內(nèi)部中止執(zhí)行線程對象

線程安全

多個線程同時訪問一塊資源, 容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全

  1. 互斥鎖 : 新線程訪問時, 發(fā)現(xiàn)其他線程正在執(zhí)行鎖定的代碼, 新線程會進入休眠
NSLock
pthread_mutex
@synchronized
  1. 自旋鎖
    忙等的鎖, 新線程會用死循環(huán)的方式, 一直等待鎖定的代碼執(zhí)行完成, 數(shù)據(jù)量少的時候用

  2. 條件鎖
    不滿足就休眠, 資源分配到了, 條件鎖打開, 進程繼續(xù)運行, 例如:NSConditionLock

  3. 讀寫鎖
    用于解決多線程對公共資源讀寫問題。 讀操作可以并發(fā)重入, 寫操作是互斥的

// 遞歸鎖
 NSRecursiveLock
 pthread_mutex(PTHREAD_MUTEX_RECURSIVE)
  1. 信號量

面試題

  1. 在不同的隊列中, 執(zhí)行100次dispatch_async 會創(chuàng)建多少個線程
    dispatch_queue_t serial = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t concurrent = dispatch_queue_create("councurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for (int i = 0; i < 100; i++) {
        
        dispatch_async(serial, ^{
            NSLog(@" 1 ----%@ ",[NSThread currentThread]);
        });
        dispatch_async(concurrent, ^{
            NSLog(@" 2----%@ ",[NSThread currentThread]);
        });
        dispatch_async(global, ^{
            NSLog(@" 3----%@ ",[NSThread currentThread]);
        });
    }

串行隊列只會創(chuàng)建一個線程, global 和 自定義 concurrent 隊列會創(chuàng)建多個線程

  1. dispatch_once 的底層實現(xiàn)
  void dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func) {
    struct _dispatch_once_waiter_s * volatile *vval = (struct _dispatch_once_waiter_s**)val;
    struct _dispatch_once_waiter_s dow = { NULL, 0 };
    struct _dispatch_once_waiter_s *tail, *tmp;
    _dispatch_thread_semaphore_t sema;
 
    if (dispatch_atomic_cmpxchg(vval, NULL, &dow)) {
        _dispatch_client_callout(ctxt, func);
        tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);
        tail = &dow;
        while (tail != tmp) {
            while (!tmp->dow_next) {
                _dispatch_hardware_pause();
            }
            sema = tmp->dow_sema;
            tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
            _dispatch_thread_semaphore_signal(sema);
        }
    } else {
        dow.dow_sema = _dispatch_get_thread_semaphore();
        for (;;) {
            tmp = *vval;
            if (tmp == DISPATCH_ONCE_DONE) {
                break;
            }
            dispatch_atomic_store_barrier();
            if (dispatch_atomic_cmpxchg(vval, tmp, &dow)) {
                dow.dow_next = tmp;
                _dispatch_thread_semaphore_wait(dow.dow_sema);
            }
        }
        _dispatch_put_thread_semaphore(dow.dow_sema);
    }
}

第一次調(diào)用: 此時外部傳進來的 onceToken 還是空指針,所以 vval 為 NULL,if 判斷成立。首先執(zhí)行 block,然后讓將 vval 的值設(shè)為 DISPATCH_ONCE_DONE 表示任務(wù)已經(jīng)完成,同時用 tmp 保存先前的 vval。此時,dow 也為空,因此 while 判斷不成立,代碼執(zhí)行結(jié)束。

同一線程第二次調(diào)用: 由于 vval 已經(jīng)變成了 DISPATCH_ONCE_DONE,因此 if 判斷不成立,進入 else 分支的 for 循環(huán)。由于 tmp 就是 DISPATCH_ONCE_DONE,所以循環(huán)退出,沒有做任何事。

多個線程同時調(diào)用: 由于 if 判斷中是一個原子性操作,所以必然只有一個線程能進入 if 分支,其他的進入 else 分支。由于其他線程在調(diào)用函數(shù)時,vval 還不是 DISPATCH_ONCE_DONE,所以進入到 for 循環(huán)的后半部分。這里構(gòu)造了一個鏈表,鏈表的每個節(jié)點上都調(diào)用了信號量的 wait 方法并阻塞,而在 if 分支中,則會依次遍歷所有的節(jié)點并調(diào)用 signal 方法,喚醒所有等待中的信號量。

  1. dispatch_barrier_async 底層實現(xiàn)
  void dispatch_barrier_async_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func) {
    dispatch_continuation_t dc;
    dc = fastpath(_dispatch_continuation_alloc_cacheonly());
    dc->do_vtable = (void *)(DISPATCH_OBJ_ASYNC_BIT | DISPATCH_OBJ_BARRIER_BIT);
    dc->dc_func = func;
    dc->dc_ctxt = ctxt;
    _dispatch_queue_push(dq, dc);
}

static _dispatch_thread_semaphore_t _dispatch_queue_drain(dispatch_queue_t dq) {
    while (dq->dq_items_tail) {
        /* ... */
        if (!DISPATCH_OBJ_IS_VTABLE(dc) && (long)dc->do_vtable & DISPATCH_OBJ_BARRIER_BIT) {
            if (dq->dq_running > 1) {
                goto out;
              }
        } else {
            _dispatch_continuation_redirect(dq, dc);
            continue;
        }
    }
out: 
    /* 不完整的 drain,需要清理現(xiàn)場 */
    return sema; // 返回空的信號量
}

void _dispatch_queue_invoke(dispatch_queue_t dq) {
    _dispatch_thread_semaphore_t sema = _dispatch_queue_drain(dq);
    if (sema) {
        _dispatch_thread_semaphore_signal(sema);
    } else if (tq) {
        return _dispatch_queue_push(tq, dq);
    }
}

do_vtable 設(shè)定了標(biāo)志位 DISPATCH_OBJ_BARRIER_BIT, 從隊列中取出任務(wù)執(zhí)行的時候遇見這個標(biāo)志位立即停止, 會終止循環(huán), 返回一個空的信號量, 然后調(diào)用 _dispatch_queue_push手動把這個任務(wù)添加進去

參考資料: http://ios.jobbole.com/88638/

  1. @synchronized 底層實現(xiàn)
  • 傳入的對象在 block 里面被釋放或者被置為 nil 會怎樣 ?

主要是調(diào)用了 objc_sync_enter 和 objc_sync_exit 方法

  @try {
    objc_sync_enter(obj);
    // do work
} @finally {
    objc_sync_exit(obj);    
}

/** 
 * Begin synchronizing on 'obj'.  
 * Allocates recursive pthread_mutex associated with 'obj' if needed.
 * 
 * @param obj The object to begin synchronizing on.
 * 
 * @return OBJC_SYNC_SUCCESS once lock is acquired.  
 */
OBJC_EXPORT  int objc_sync_enter(id obj)
    __OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_2_0);

/** 
 * End synchronizing on 'obj'. 
 * 
 * @param obj The objet to end synchronizing on.
 * 
 * @return OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
 */
OBJC_EXPORT  int objc_sync_exit(id obj)
    __OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_2_0);

typedef struct SyncData {
    id object;
    recursive_mutex_t mutex;
    struct SyncData* nextData;
    int threadCount;
} SyncData;

typedef struct SyncList {
    SyncData *data;
    spinlock_t lock;
} SyncList;

// Use multiple parallel lists to decrease contention among unrelated objects.
#define COUNT 16
#define HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))
#define LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock
#define LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].data
static SyncList sDataLists[COUNT];

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");
    
        result = recursive_mutex_lock(&data->mutex);
        require_noerr_string(result, done, "mutex_lock failed");
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

done: 
    return result;
}

int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR, "id2data failed");
        
        result = recursive_mutex_unlock(&data->mutex);
        require_noerr_string(result, done, "mutex_unlock failed");
    } else {
        // @synchronized(nil) does nothing
    }
    
done:
    if ( result == RECURSIVE_MUTEX_NOT_LOCKED )
         result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;

    return result;
}

定義哈希算法將傳入的對象映射到數(shù)組上的一個下標(biāo), 對象指針在內(nèi)存的地址轉(zhuǎn)變成無符號整形并有移5位, 再跟 COUNT 做 & 運算, 這樣結(jié)果不會超出數(shù)組大小

使用遞歸鎖 mutex 來做同步, @synchronized(nil) 不起任何作用
如果在block 里面?zhèn)魅肓薾il, 將會從代碼中移走線程安全, 調(diào)用objc_sync_exit 方法時候, 不做解鎖處理

  1. dispatch_async 的底層實現(xiàn)
    隊列是用來提交 block 的對象, 按照先入先出的順序進行處理, GCD 底層會維護一個線程池, 用來執(zhí)行這些 bock。
  void dispatch_async_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func) {
    dispatch_continuation_t dc;
    if (dq->dq_width == 1) {
        return dispatch_barrier_async_f(dq, ctxt, func);
    }
    dc->do_vtable = (void *)DISPATCH_OBJ_ASYNC_BIT;
    dc->dc_func = func;
    dc->dc_ctxt = ctxt;
    if (dq->do_targetq) {
        return _dispatch_async_f2(dq, dc);
    }
    _dispatch_queue_push(dq, dc);
}

如果是串行隊列 dq_with == 1, 調(diào)用 dispatch_barrier_async_f 函數(shù)處理, 如果有 do_targetq 則進行轉(zhuǎn)發(fā), 否則調(diào)用 _dispatch_queue_push 入隊

用鏈表保存所有提交的 block,然后在底層線程池中,依次取出 block 并執(zhí)行

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

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

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