美文网首页面试题答案Python学习
《iOS面试题整理》- GCD、多线程相关面试题

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

作者: 小木头 | 来源:发表于2019-01-28 21:13 被阅读19次

    基本概念

    进程和线程的区别

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

    进程的状态

    • 新建
    • 就绪 : 线程对象加入线程池中等待 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、多线程相关面试题

        本文链接:https://www.haomeiwen.com/subject/wnhejqtx.html