需要关于调度队列、线程和NSRunLoop的一些澄清。

32

我所知道和理解的如下:

全局队列是一个并发队列,可以将任务分派到多个线程。执行任务的顺序不保证。例如:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), {
 for (int i; i<10; i++) {
  doTask()
 }
})

如果我想分派到串行队列,我可以使用

dispatch_async(dispatch_queue_create("my.serial.queue", nil) {
  ...
}

每次只有一个任务被分派到一个线程并执行,顺序为FIFO。

===== 我困惑和不完全理解的地方 ========

  1. 主线程有一个NSRunLoop,在主线程循环任务。我想知道调度队列和运行循环之间的关系是什么?能否这样理解:如果将任务分派到主线程,则主线程的NSRunLoop接收分派的任务并执行它?

  2. 那么分派任务到多个线程的全局队列呢?iOS/OSX系统会自动为每个线程创建NSRunLoop吗?然后每个线程中的运行循环从全局队列中获取分派的任务并执行?

  3. 谁知道线程?dispatch_async()dispatch_sync() 函数是否知道将任务分派到哪个线程,或者是队列知道将任务分派到哪个线程?

  4. 是否有一种程序化的方法可以从调度队列中获取NSRunLoop对象(分派任务的线程)?(此问题与问题3有关)


你为什么想在后台线程上使用一个runloop呢?如果你描述一下你想做什么,我们也许可以提供更好的解决方案。 - Rob
好的。底线是,您不需要(也不一定希望)在每个线程上运行循环。运行循环的目的是保持线程活动,轮询事件。这几乎与GCD模式背道而驰。 - Rob
@Rob,我还是不太清楚。就像你说的,运行循环的目的是保持线程活动并轮询事件。好的,那么,我必须再问一遍:GCD创建的线程(在调度任务时)是否由iOS/OSX创建了运行循环?如果答案是否定的,那么这个线程如何轮询分派给它的任务呢?(我并不是说我需要那个运行循环,我只是想知道当从队列中分派任务到线程时,iOS/OSX是否会创建它。) - Leem.fin
谢谢@Rob,现在我明白了。 - Leem.fin
让我们在聊天中继续这个讨论:点击此处进入聊天室 - Rob
显示剩余4条评论
2个回答

38
  1. 主线程的运行循环有一个步骤,它会运行在主队列上排队的任何块。如果您想了解运行循环的详细内容,可以参考this answer

  2. GCD为并发队列创建线程。直到第一次运行在该线程上的任务请求该线程的运行循环时,该线程才具有运行循环,此时系统会为该线程创建一个运行循环。但是,只有当该线程上的某些东西要求其运行(通过调用-[NSRunLoop run]CFRunLoopRun等)时,运行循环才会运行。大多数线程,包括为GCD队列创建的线程,都没有运行循环。

  3. GCD管理一个线程池,当需要运行块(因为它被添加到某个队列中)时,GCD选择要在哪个线程上运行该块。 GCD的线程选择算法基本上是实现细节,除了它将始终为添加到主队列的块选择主线程之外。(请注意,GCD有时也会使用主线程来运行添加到其他队列的块。)

  4. 您只能获取主线程的运行循环(使用+[NSRunLoop mainRunLoop]CFRunLoopGetMain)或当前线程的运行循环(使用+[NSRunLoop currentRunLoop]CFRunLoopGetCurrent)。如果需要某个任意线程的运行循环,则必须找到一种方法在该线程上调用CFRunLoopGetCurrent并以安全、同步的方式将其返回值传递回跨线程。

    请注意,NSRunLoop接口不是线程安全的,但CFRunLoop接口线程安全的,因此如果需要访问另一个线程的运行循环,则应使用CFRunLoop接口。

    还要注意,您可能不应该在运行在GCD队列上的块中长时间运行运行循环,因为这会占用GCD希望控制的线程。如果需要长时间运行运行循环,则应为其启动自己的线程。您可以在CFStream.c中的_legacyStreamRunLoop函数中看到示例。请注意,它如何在dispatch_semaphore_t的保护下初始化名为sLegacyRL的静态变量,使专用线程的运行循环可用。


谢谢您的解释,我会阅读您提供的链接,如果有问题,我会在这里评论。 - Leem.fin
你能详细说明一下“请注意,GCD有时也会使用主线程来执行添加到其他队列中的块。”这是否与UI相关? - abc123
dispatch_sync文档中可以得知:“作为一种优化,此函数在可能的情况下会在当前线程上调用块。” - rob mayoff
谢谢,有一个问题。在 Swift 中,NSRunLoop 经常使用吗?我使用 GCD 将一些重任务放入后台队列并推回主线程进行 UI 处理,以避免屏幕冻结。从未在项目中使用过 NSRunLoop。您可以解释一下应该在哪种情况下使用 NSRunLoop,它能帮助什么吗? - Zhou Haibo
运行循环是一个大的主题。评论不是解释它的合适场所。 - rob mayoff

9
  1. 主线程的run loop和主调度队列之间的关系仅在于它们都在主线程上运行,而分派到主队列的块会与在主run loop上处理的事件交错执行。

    正如并发编程指南所述:

    主调度队列是一个全局可用的串行队列,它在应用程序的主线程上执行任务。如果存在,则此队列与应用程序的运行循环一起工作,以将排队的任务的执行与附加到运行循环的其他事件源的执行交错。由于它在您的应用程序的主线程上运行,因此主队列通常用作应用程序的同步点。

  2. 当分派到后台线程时,它不会为这些工作线程创建NSRunLoop。通常情况下,这些后台线程也不需要运行循环。我们过去必须为后台线程创建自己的NSRunLoop(例如,在后台线程上调度NSURLConnection时),但是这种模式现在不再经常需要。

    对于历史上需要运行循环的事情,如果在后台线程上运行它们,则通常有更好的机制。例如,现在您会使用NSURLSession而不是NSURLConnection。或者,您会创建一个GCD定时器分派源,而不是在后台线程上的NSRunLoop上使用NSTimer

  3. 关于谁“知道”线程,当分派到队列时,工作线程被标识出来。线程不是队列的属性,而是在队列需要时从线程池中选择一个线程。

  4. 如果要为工作线程创建NSRunLoop(通常情况下您不应该这样做),则需要自己创建并跟踪它。如果调用current,它将为您创建一个运行循环:“如果尚未为线程创建运行循环,则会创建并返回一个。”

    当使用运行循环安排线程时,我倾向于自己创建NSThread并在其上安排运行循环,而不是占用GCD非常有限的工作线程之一。


关于第四点。我的主要问题不是如何为工作线程创建NSRunLoop。我想问的是,每个由GCD在调度任务时创建的线程是否都有一个NSRunLoop?如果是这样,我该如何从调度队列中获取这个NSRunLoop? - Leem.fin
1
每个线程没有自己的运行循环,因此通常没有什么可以检索的内容。有一种方法可以检索主运行循环,但除此之外,唯一存在的运行循环是你手动创建的那些。 - Rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接