iOS主线程和主队列的区别

这个问题之前也有看到,正好这两天看到一篇文章提到这个文艺,就深入的研究了一下,地址我的同事金司机出的 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容