iOS主线程和主队列的区别

作者: 小凉介 | 来源:发表于2018-08-25 15:55 被阅读340次

    这个问题之前也有看到,正好这两天看到一篇文章提到这个文艺,就深入的研究了一下,地址我的同事金司机出的 5 道 iOS 多线程“面试题”,其中第一题和第二题就是考察主线程和主队列区别的。

    问题

    第一题(主线程只会执行主队列的任务吗?)

    let key = DispatchSpecificKey<String>()
    
    DispatchQueue.main.setSpecific(key: key, value: "main")
    
    func log() {
        debugPrint("main thread: \(Thread.isMainThread)")
        let value = DispatchQueue.getSpecific(key: key)
        debugPrint("main queue: \(value != nil)")
    }
    
    DispatchQueue.global().sync(execute: log)
    RunLoop.current.run()
    

    执行结果是什么?

    第二题(主队列任务只会在主线程上执行吗)

    let key = DispatchSpecificKey<String>()
    
    DispatchQueue.main.setSpecific(key: key, value: "main")
    
    func log() {
      debugPrint("main thread: \(Thread.isMainThread)")
      let value = DispatchQueue.getSpecific(key: key)
      debugPrint("main queue: \(value != nil)")
    }
    
    DispatchQueue.global().async {
      DispatchQueue.main.async(execute: log)
    }
    dispatchMain()
    
    

    执行结果是什么?

    解答

    第一题

    结果:

    "main thread: true"
    "main queue: false"
    
    

    我们可以看swift-corelibs-libdispatch的一个PR10.6.2: Always run dispatch_sync blocks on the current thread to bett…

    static void
    _dispatch_barrier_sync_f_slow(dispatch_queue_t dq, void *ctxt, dispatch_function_t func)
    {
        
        // It's preferred to execute synchronous blocks on the current thread
        // due to thread-local side effects, garbage collection, etc. However,
        // blocks submitted to the main thread MUST be run on the main thread
        
        struct dispatch_barrier_sync_slow2_s dbss2 = {
            .dbss2_dq = dq,
            .dbss2_func = func,
            .dbss2_ctxt = ctxt,
            .dbss2_ctxt = ctxt,     
            .dbss2_sema = _dispatch_get_thread_semaphore(),
        };
        struct dispatch_barrier_sync_slow_s {
    @@ -746,17 +759,17 @@ _dispatch_barrier_sync_f_slow(dispatch_queue_t dq, void *ctxt, dispatch_function
            .dc_func = _dispatch_barrier_sync_f_slow_invoke,
            .dc_ctxt = &dbss2,
        };
        
        dispatch_queue_t old_dq = _dispatch_thread_getspecific(dispatch_queue_key);
        _dispatch_queue_push(dq, (void *)&dbss);
        dispatch_semaphore_wait(dbss2.dbss2_sema, DISPATCH_TIME_FOREVER);
        while (dispatch_semaphore_wait(dbss2.dbss2_sema, dispatch_time(0, 3ull * NSEC_PER_SEC))) {
            if (DISPATCH_OBJECT_SUSPENDED(dq)) {
                continue;
            }
            if (_dispatch_queue_trylock(dq)) {
                _dispatch_queue_drain(dq);
                _dispatch_queue_unlock(dq);
            }
        if (dq != dispatch_get_main_queue()) {
            _dispatch_thread_setspecific(dispatch_queue_key, dq);
            func(ctxt);
            _dispatch_workitem_inc();
            _dispatch_thread_setspecific(dispatch_queue_key, old_dq);
            dispatch_resume(dq);
        }
        _dispatch_put_thread_semaphore(dbss2.dbss2_sema);
    }
    
    

    It's preferred to execute synchronous blocks on the current thread
    due to thread-local side effects, garbage collection, etc.

    DispatchQueue.global().sync会阻塞当前线程MainThread,那加入DispatchQueue.global的任务会在哪个线程执行呢?
    苹果的解释是为了性能,因为线程切换是好性能的,在当前线程MainThread中执行任务。下面这一部分会介绍一下到底是怎样线程切换性能的原因,内容主要来自于
    欧阳大哥深入iOS系统底层之CPU寄存器介绍一文,这篇文章写的非常好,个人很是喜爱,其中有这么一段:

    线程切换时的寄存器复用
    我们的代码并不是只在单线程中执行,而是可能在多个线程中执行。那么这里你就可能会产生一个疑问?既然进程中有多个线程在并行执行,而CPU中的寄存器又只有那么一套,如果不加处理岂不会产生数据错乱的场景?答案是否定的。我们知道线程是一个进程中的执行单元,每个线程的调度执行其实都是通过操作系统来完成。也就是说哪个线程占有CPU执行以及执行多久都是由操作系统控制的。具体的实现是每创建一个线程时都会为这线程创建一个数据结构来保存这个线程的信息,我们称这个数据结构为线程上下文,每个线程的上下文中有一部分数据是用来保存当前所有寄存器的副本。每当操作系统暂停一个线程时,就会将CPU中的所有寄存器的当前内容都保存到线程上下文数据结构中。而操作系统要让另外一个线程执行时则将要执行的线程的上下文中保存的所有寄存器的内容再写回到CPU中,并将要运行的线程中上次保存暂停的指令也赋值给CPU的指令寄存器,并让新线程再次执行。可以看出操作系统正是通过这种机制保证了即使是多线程运行时也不会导致寄存器的内容发生错乱的问题。因为每当线程切换时操作系统都帮它们将数据处理好了。下面的部分线程上下文结构正是指定了所有寄存器信息的部分:

    //这个结构是linux在arm32CPU上的线程上下文结构,代码来自于:http://elixir.free-electrons.com/linux/latest/source/arch/arm/include/asm/thread_info.h  
    //这里并没有保存所有的寄存器,是因为ABI中定义linux在arm上运行时所使用的寄存器并不是全体寄存器,所以只需要保存规定的寄存器的内容即可。这里并不是所有的CPU所保存的内容都是一致的,保存的内容会根据CPU架构的差异而不同。
    //因为iOS的内核并未开源所以无法得到iOS定义的线程上下文结构。
    
    //线程切换时要保存的CPU寄存器,
    struct cpu_context_save {
        __u32   r4;
        __u32   r5;
        __u32   r6;
        __u32   r7;
        __u32   r8;
        __u32   r9;
        __u32   sl;
        __u32   fp;
        __u32   sp;
        __u32   pc;
        __u32   extra[2];       /* Xscale 'acc' register, etc */
    };
    
    //线程上下文结构
    struct thread_info {
        unsigned long       flags;      /* low level flags */
        int         preempt_count;  /* 0 => preemptable, <0 => bug */
        mm_segment_t        addr_limit; /* address limit */
        struct task_struct  *task;      /* main task structure */
        __u32           cpu;        /* cpu */
        __u32           cpu_domain; /* cpu domain */
        struct cpu_context_save cpu_context;    /* cpu context */
        __u32           syscall;    /* syscall number */
        __u8            used_cp[16];    /* thread used copro */
        unsigned long       tp_value[2];    /* TLS registers */
    #ifdef CONFIG_CRUNCH
        struct crunch_state crunchstate;
    #endif
        union fp_state      fpstate __attribute__((aligned(8)));  /*浮点寄存器*/
        union vfp_state     vfpstate;  /*向量浮点寄存器*/
    #ifdef CONFIG_ARM_THUMBEE
        unsigned long       thumbee_state;  /* ThumbEE Handler Base register */
    #endif
    };
    
    
    
    1432482-45c252046d51cb9e.png

    最后引申出个很经典的问题,就是苹果的MapKit / VektorKit,它在底层实现的时候,不仅仅要求代码执行在主线程上,还要求执行在 GCD 的主队列上。所以只是在执行的时候判断当前是不是主线程是不够的,需要判断当前是不是在主队列上,那怎么判断呢?
    GCD没有提供API来进行判断当前执行任务是在什么队列,但是我们可以利用dispatch_queue_set_specificdispatch_get_specific这一组方法为主队列打上标记,这里是RxSwift判断是否是主队列的代码:

    extension DispatchQueue {
        private static var token: DispatchSpecificKey<()> = {
            let key = DispatchSpecificKey<()>()
            DispatchQueue.main.setSpecific(key: key, value: ())
            return key
        }()
    
        static var isMain: Bool {
            return DispatchQueue.getSpecific(key: token) != nil
        }
    }
    
    

    第二题

    结果:

    "main thread: false"
    "main queue: true"
    

    当我把dispatchMain()删掉之后打印出来的结果是这样

    "main thread: true"
    "main queue: true"
    

    所以可以肯定是dispatchMain()在作怪。

    之后我再加这段代码

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("-----")
    }
    

    打印了这些

    "main thread: false"
    "main queue: true"
    2018-08-23 14:13:19.281103+0800 MainThread[2720:5940476] [general] Attempting to wake up main runloop, but the main thread as exited. This message will only log once. Break on _CFRunLoopError_MainThreadHasExited to debug.
    
    

    同时Google到这些解释

    You don't generally want to use dispatch_main(). It's for things other than regular applications (system daemons and such). It is, in fact, guaranteed to break your program if you call it in a regular app.

    dispatch_main() is not for running things on the main thread — it runs the GCD block dispatcher. In a normal app, you won't need or want to use it.

    还查到这个是OSX服务程序使用,iOS不使用。通过上面的解释我猜测主队列任务通常是在主线程执行,但是当遇到这种主线程已经退出的情形,比如执行了dispatchMain(),苹果在底层选择让其他线程来执行主线程的任务。

    在看苹果源码看到这一段swift-corelibs-libdispatch

    void
    dispatch_barrier_sync(dispatch_queue_t dq, void (^work)(void))
    {
        // Blocks submitted to the main queue MUST be run on the main thread,
        // therefore we must Block_copy in order to notify the thread-local
        // garbage collector that the objects are transferring to the main thread
        if (dq == dispatch_get_main_queue()) {
            dispatch_block_t block = Block_copy(work);
            return dispatch_barrier_sync_f(dq, block, _dispatch_call_block_and_release);
        }   
        struct Block_basic *bb = (void *)work;
        dispatch_barrier_sync_f(dq, work, (dispatch_function_t)bb->Block_invoke);
    
    

    Blocks submitted to the main queue MUST be run on the main thread

    虽然不够严谨,但在iOS系统上可以说主队列任务只会在主线程上执行

    参考文章
    深入iOS系统底层之CPU寄存器介绍
    深入浅出GCD之dispatch_queue
    深入理解 GCD
    GCD's Main Queue vs. Main Thread
    奇怪的GCD

    相关文章

      网友评论

      本文标题:iOS主线程和主队列的区别

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