《iOS面试题整理》- GCD、多线程相关面试题

基本概念

进程和线程的区别

  • 进程是指系统中正在运行的一个应用程序, 每个进程之间是相互独立的
  • 一个进程中可以有多条线程, 进程的所有任务都在线程中执行的

进程的状态

  • 新建
  • 就绪 : 线程对象加入线程池中等待 CPU 调度
  • 运行 : CPU负责调度线程中线程的执行, 线程执行完成前, 状态可能在就绪和运行之间来回切换
  • 阻塞 : 满足某个预定条件, 使用休眠或锁, 阻塞线程执行
  • 死亡 : 线程执行完毕, 或者内部中止执行线程对象

线程安全

多个线程同时访问一块资源, 容易引发数据错乱和数据安全

  1. 互斥锁 : 新线程访问时, 发现其他线程正在执行锁定的代码, 新线程会进入休眠
NSLock
pthread_mutex
@synchronized
  1. 自旋锁
    忙等的锁, 新线程会用死循环的方式, 一直等待锁定的代码执行完成, 数据量少的时候用

  2. 条件锁
    不满足就休眠, 资源分配到了, 条件锁打开, 进程继续运行, 例如:NSConditionLock

  3. 读写锁
    用于解决多线程对公共资源读写问题。 读操作可以并发重入, 写操作是互斥的

// 递归锁
 NSRecursiveLock
 pthread_mutex(PTHREAD_MUTEX_RECURSIVE)
  1. 信号量

面试题

  1. 在不同的队列中, 执行100次dispatch_async 会创建多少个线程
    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]);
        });
    }

串行队列只会创建一个线程, global 和 自定义 concurrent 队列会创建多个线程

  1. dispatch_once 的底层实现
  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);
    }
}

第一次调用: 此时外部传进来的 onceToken 还是空指针,所以 vval 为 NULL,if 判断成立。首先执行 block,然后让将 vval 的值设为 DISPATCH_ONCE_DONE 表示任务已经完成,同时用 tmp 保存先前的 vval。此时,dow 也为空,因此 while 判断不成立,代码执行结束。

同一线程第二次调用: 由于 vval 已经变成了 DISPATCH_ONCE_DONE,因此 if 判断不成立,进入 else 分支的 for 循环。由于 tmp 就是 DISPATCH_ONCE_DONE,所以循环退出,没有做任何事。

多个线程同时调用: 由于 if 判断中是一个原子性操作,所以必然只有一个线程能进入 if 分支,其他的进入 else 分支。由于其他线程在调用函数时,vval 还不是 DISPATCH_ONCE_DONE,所以进入到 for 循环的后半部分。这里构造了一个链表,链表的每个节点上都调用了信号量的 wait 方法并阻塞,而在 if 分支中,则会依次遍历所有的节点并调用 signal 方法,唤醒所有等待中的信号量。

  1. dispatch_barrier_async 底层实现
  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,需要清理现场 */
    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 设定了标志位 DISPATCH_OBJ_BARRIER_BIT, 从队列中取出任务执行的时候遇见这个标志位立即停止, 会终止循环, 返回一个空的信号量, 然后调用 _dispatch_queue_push手动把这个任务添加进去

参考资料: http://ios.jobbole.com/88638/

  1. @synchronized 底层实现
  • 传入的对象在 block 里面被释放或者被置为 nil 会怎样 ?

主要是调用了 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;
}

定义哈希算法将传入的对象映射到数组上的一个下标, 对象指针在内存的地址转变成无符号整形并有移5位, 再跟 COUNT 做 & 运算, 这样结果不会超出数组大小

使用递归锁 mutex 来做同步, @synchronized(nil) 不起任何作用
如果在block 里面传入了nil, 将会从代码中移走线程安全, 调用objc_sync_exit 方法时候, 不做解锁处理

  1. dispatch_async 的底层实现
    队列是用来提交 block 的对象, 按照先入先出的顺序进行处理, GCD 底层会维护一个线程池, 用来执行这些 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, 调用 dispatch_barrier_async_f 函数处理, 如果有 do_targetq 则进行转发, 否则调用 _dispatch_queue_push 入队

用链表保存所有提交的 block,然后在底层线程池中,依次取出 block 并执行

你可能感兴趣的:(《iOS面试题整理》- GCD、多线程相关面试题)