iOS底层原理探索 之 GCD函数和队列

1,383 阅读23分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载
  15. iOS 底层原理探索 之 分类的加载
  16. iOS 底层原理探索 之 关联对象
  17. iOS底层原理探索 之 魔法师KVC
  18. iOS底层原理探索 之 KVO原理|8月更文挑战
  19. iOS底层原理探索 之 重写KVO|8月更文挑战
  20. iOS底层原理探索 之 多线程原理|8月更文挑战

以上内容的总结专栏


细枝末节整理


前言

iOS底层原理探索 之 多线程原理|8月更文挑战中,我们整理了线程的生命周期、线程与进程的概念和他们之间的关系。并且关于多线程的意义和原理也有涉及。

其实在苹果的线程编程指南中,有涉及到关于线程的替代品的章节。因为自己创建线程的一个问题是它们会给开发者的代码增加不确定性。线程是在应用程序中支持并发性的一种相对低级和复杂的方式。如果我们不完全理解我们对于选择的含义,那么,将很容易遇到在同步或计时的问题,这种问题的严重程度很有可能从细微的行为变化到应用程序的崩溃和数据的损坏。所以,今天我们来关注下替代线程的众多技术中,我们用到最多的GCD,探索下GCD的函数和队列的内容。

调度队列

Grand Central Dispatch (GCD) 调度队列是执行任务的强大工具。调度队列让你可以异步或同步地执行任意代码块,无论是与调用者相关的。您可以使用调度队列来执行您过去在单独线程上执行的几乎所有任务。调度队列的优点是它们比相应的线程代码更易于使用并且在执行这些任务时效率更高。

关于调度队列

调度队列是一种在应用程序中异步和并发执行任务的简单方法。一个任务很简单,就是你的应用程序需要执行一些工作。例如,您可以定义一个任务来执行一些计算、创建或修改数据结构、处理从文件中读取的一些数据或任何数量的事情。您可以通过将相应的代码放置在函数或块对象中并将其添加到调度队列来定义任务。

调度队列是一种类似对象的结构,用于管理您提交给它的任务。所有的调度队列都是先进先出的数据结构。因此,您添加到队列中的任务始终以添加它们的相同顺序启动。GCD 会自动为您提供一些调度队列,但您可以为特定目的创建其他队列。表 3-1列出了您的应用程序可用的调度队列类型以及您如何使用它们。

表 3-1  调度队列类型

类型描述
串行 (Serial)串行队列(也称为私有调度队列)按添加到队列的顺序一次执行一项任务。当前正在执行的任务在由调度队列管理的不同线程上运行(可能因任务而异)。串行队列通常用于同步对特定资源的访问。您可以根据需要创建任意数量的串行队列,并且每个队列相对于所有其他队列同时运行。换句话说,如果您创建四个串行队列,则每个队列一次仅执行一项任务,但最多仍可以同时执行四个任务,每个队列一个。有关如何创建串行队列的信息,请参阅创建串行调度队列
并发 (Concurrent)并发队列(也称为一种全局调度队列)并发执行一个或多个任务,但任务仍然按照它们添加到队列的顺序启动。当前正在执行的任务在由调度队列管理的不同线程上运行。在任何给定点执行的确切任务数量是可变的,取决于系统条件。在 iOS 5 及更高版本中,您可以通过指定DISPATCH_QUEUE_CONCURRENT队列类型来自己创建并发调度队列。此外,还有四个预定义的全局并发队列供您的应用程序使用。有关如何获取全局并发队列的更多信息,请参阅获取全局并发调度队列
主调度队列 (Main dispatch queue)主调度队列是一个全局可用的串行队列,它在应用程序的主线程上执行任务。该队列与应用程序的运行循环(如果存在)一起工作,以将排队任务的执行与附加到运行循环的其他事件源的执行交错​​。因为它在应用程序的主线程上运行,所以主队列通常用作应用程序的关键同步点。虽然您不需要创建主调度队列,但您确实需要确保您的应用程序适当地排空它。有关如何管理此队列的更多信息,请参阅在主线程上执行任务

在向应用程序添加并发性时,调度队列比线程提供了几个优势。最直接的优点是工作队列编程模型的简单性。对于线程,您必须为要执行的工作以及线程本身的创建和管理编写代码。调度队列让你专注于你真正想要执行的工作,而不必担心线程的创建和管理。相反,系统会为您处理所有线程的创建和管理。优点是系统能够比任何单个应用程序更有效地管理线程。系统可以根据可用资源和当前系统条件动态调整线程数量。此外,

尽管您可能认为为调度队列重写代码会很困难,但为调度队列编写代码通常比为线程编写代码更容易。编写代码的关键是设计自包含且能够异步运行的任务。(实际上对于线程和调度队列都是如此。)然而,调度队列的优势在于可预测性。如果您有两个任务访问相同的共享资源但在不同的线程上运行,则任一线程都可以先修改资源,您需要使用锁来确保两个任务不会同时修改该资源。使用调度队列,您可以将两个任务添加到串行调度队列,以确保在任何给定时间只有一个任务修改资源。

尽管您指出在串行队列中运行的两个任务不会同时运行是正确的,但您必须记住,如果两个线程同时获取锁,线程提供的任何并发性都会丢失或显着减少。更重要的是,线程模型需要创建两个线程,它们占用内核空间和用户空间内存。调度队列不会为其线程支付相同的内存损失,并且它们确实使用的线程保持忙碌而不被阻塞。

关于调度队列要记住的其他一些关键点包括:

  • 调度队列相对于其他调度队列并发执行它们的任务。任务的序列化仅限于单个调度队列中的任务。
  • 系统确定任一时间执行的任务总数。因此,具有 100 个不同队列中的 100 个任务的应用程序可能不会同时执行所有这些任务(除非它具有 100 个或更多有效内核)。
  • 在选择启动哪些新任务时,系统会考虑队列优先级。有关如何设置串行队列优先级的信息,请参阅为队列提供清理功能
  • 队列中的任务在添加到队列时必须准备好执行。(如果您之前使用过 Cocoa 操作对象,请注意此行为与模型操作使用的不同。)
  • 私有调度队列是引用计数的对象。除了在您自己的代码中保留队列之外,请注意调度源也可以附加到队列并增加其保留计数。因此,您必须确保取消所有调度源,并且所有保留调用都与适当的释放调用相平衡。有关保留和释放队列的更多信息,请参阅Dispatch Queues 的内存管理。有关调度源的更多信息,请参阅关于调度源

队列相关技术

除了调度队列之外,Grand Central Dispatch 还提供了多种使用队列来帮助管理代码的技术。表 3-2列出了这些技术,并提供了指向您可以找到有关它们的更多信息的链接。

表3-2 使用调度队列的技术

技术描述
调度组 (Dispatch groups)调度组是一种监视一组块对象是否完成的方法。(您可以根据需要同步或异步监视块。)组为依赖于其他任务完成的代码提供了有用的同步机制。有关使用组的更多信息,请参阅等待队列任务组
信号量 (Dispatch semaphores)调度信号量类似于传统的信号量,但通常更有效。仅当调用线程因信号量不可用而需要被阻塞时,调度信号量才会向下调用内核。如果信号量可用,则不进行内核调用。有关如何使用调度信号量的示例,请参阅使用调度信号量来规范有限资源的使用
调度源 (Dispatch sources)调度源生成通知以响应特定类型的系统事件。您可以使用调度源来监视事件,例如进程通知、信号和描述符事件等。当事件发生时,调度源将您的任务代码异步提交到指定的调度队列进行处理。有关创建和使用调度源的更多信息,请参阅调度源

使用块(Block)实现任务

块对象是一种基于 C 的语言功能,您可以在 C、 Objective-C和 C++ 代码中使用它。块可以很容易地定义一个独立的工作单元。尽管它们看起来类似于函数指针,但块实际上是由类似于对象的底层数据结构表示的,并且由编译器为您创建和管理。编译器将您提供的代码(以及任何相关数据)打包,并将其封装为可以存在于堆中并在您的应用程序中传递的形式。

块的主要优点之一是它们能够使用其词法范围之外的变量。当您在函数或方法中定义块时,该块在某些方面充当传统代码块。例如,一个块可以读取在父作用域中定义的变量的值。块访问的变量被复制到堆上的块数据结构中,以便块以后可以访问它们。当块被添加到调度队列时,这些值通常必须保留为只读格式。但是,同步执行的块也可以使用带有__block关键字的变量来将数据返回到父级的调用范围。

您可以使用与用于函数指针的语法类似的语法在代码中内联声明块。块和函数指针之间的主要区别在于块名称前面是插入符号 ( ^) 而不是星号 ( *)。就像函数指针一样,您可以将参数传递给块并从中接收返回值。清单 3-1向您展示了如何在代码中同步声明和执行块。该变量aBlock被声明为一个接受单个整数参数且不返回任何值的块。然后将匹配该原型的实际块分配给aBlock并声明为内联。最后一行立即执行块,将指定的整数打印到标准输出。

清单 3-1  一个简单的块示例

int x = 123;
int y = 456;
 
// Block 声明和赋值
void (^aBlock)(int) = ^(int z) {
    printf("%d %d %d\n", x, y, z);
};
 
// Block 执行
aBlock(789);   // prints: 123 456 789

以下是设计模块时应考虑的一些关键准则的摘要:

  • 对于您计划使用调度队列异步执行的块,从父函数或方法中捕获标量变量并在块中使用它们是安全的。但是,您不应尝试捕获由调用上下文分配和删除的大型结构或其他基于指针的变量。当你的块被执行时,该指针引用的内存可能已经消失了。当然,自己分配内存(或对象)并将该内存的所有权明确移交给块是安全的。
  • 调度队列复制添加到其中的块,并在完成执行后释放块。换句话说,在将块添加到队列之前,您不需要显式复制块。
  • 尽管在执行小任务时队列比原始线程更有效,但创建块并在队列上执行它们仍然存在开销。如果一个块做的工作太少,内联执行它可能比将它分派到队列更便宜。判断块是否做的工作太少的方法是使用性能工具收集每个路径的指标并进行比较。
  • 不要缓存与底层线程相关的数据,并期望可以从不同的块访问该数据。如果同一队列中的任务需要共享数据,则使用调度队列的上下文指针来存储数据。
  • 如果您的块创建了多个 Objective-C 对象,您可能希望将块代码的一部分包含在 @autorelease 块中,以处理这些对象的内存管理。尽管 GCD 调度队列有自己的自动释放池,但它们不能保证这些池何时被排空。如果您的应用程序内存受限,创建您自己的自动释放池可以让您以更规律的时间间隔为自动释放对象释放内存。

创建和管理调度队列

在将任务添加到队列之前,您必须决定要使用的队列类型以及打算如何使用它。调度队列可以串行或并发地执行任务。此外,如果您考虑到队列的特定用途,则可以相应地配置队列属性。以下部分向您展示如何创建调度队列并配置它们以供使用。

获取全局并发调度队列

当您有多个可以并行运行的任务时,并发调度队列很有用。并发队列仍然是一个队列,因为它以先进先出的顺序将任务出列;但是,并发队列可能会在任何先前的任务完成之前使其他任务出列。并发队列在任何给定时刻执行的实际任务数是可变的,并且可以随着应用程序中条件的变化而动态变化。许多因素会影响并发队列执行的任务数量,包括可用内核的数量、其他进程正在完成的工作量以及其他串行调度队列中任务的数量和优先级。

系统为每个应用程序提供了四个并发调度队列。这些队列对应用程序是全局的,仅通过它们的优先级来区分。因为它们是全局的,所以您不必显式地创建它们。相反,您使用该dispatch_get_global_queue函数请求队列之一,如以下示例所示:

dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

除了获取默认的并发队列之外,您还可以通过将DISPATCH_QUEUE_PRIORITY_HIGHDISPATCH_QUEUE_PRIORITY_LOW常量传递给函数来获取具有高优先级和低优先级的队列,或者通过传递常量来获取后台队列DISPATCH_QUEUE_PRIORITY_BACKGROUND。如您所料,高优先级并发队列中的任务先于默认队列和低优先级队列中的任务执行。同样,默认队列中的任务在低优先级队列中的任务之前执行。

注意: dispatch_get_global_queue函数 的第二个参数是为将来扩展保留的。现在,您应该始终通过0此参数。

尽管调度队列是引用计数对象,但您不需要保留和释放全局并发队列。因为它们对您的应用程序是全局的,所以这些队列的保留和释放调用将被忽略。因此,您不需要存储对这些队列的引用。dispatch_get_global_queue只要您需要对其中之一的引用,就可以调用该函数。

创建串行调度队列

当您希望任务以特定顺序执行时,串行队列很有用。串行队列一次只执行一个任务,并且总是从队列的头部拉取任务。您可以使用串行队列而不是锁来保护共享资源或可变数据结构。与锁不同,串行队列确保任务以可预测的顺序执行。而且只要您将任务异步提交到串行队列,队列就永远不会死锁。

与为您创建的并发队列不同,您必须显式创建和管理要使用的任何串行队列。您可以为您的应用程序创建任意数量的串行队列,但应避免创建大量串行队列仅作为同时执行尽可能多的任务的手段。如果要并发执行大量任务,请将它们提交到全局并发队列之一。创建串行队列时,请尝试确定每个队列的用途,例如保护资源或同步应用程序的某些关键行为。

清单 3-2显示了创建自定义串行队列所需的步骤。该dispatch_queue_create函数采用两个参数:队列名称和一组队列属性。调试器和性能工具显示队列名称以帮助您跟踪任务的执行方式。队列属性保留供将来使用,应该是NULL.

清单 3-2  创建一个新的串行队列

dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);

除了您创建的任何自定义队列之外,系统还会自动创建一个串行队列并将其绑定到您的应用程序的主线程。有关获取主线程队列的更多信息,请参阅在运行时获取公共队列

在运行时获取公共队列

Grand Central Dispatch 提供的函数让您可以从应用程序访问几个常见的调度队列:

  • 将该dispatch_get_current_queue函数用于调试目的或测试当前队列的身份。从块对象内部调用此函数会返回块提交到的队列(并且它现在可能正在运行)。从块外部调用此函数会返回应用程序的默认并发队列。
  • 使用该dispatch_get_main_queue函数获取与应用程序主线程关联的串行调度队列。该队列是为 Cocoa 应用程序和在主线程上调用dispatch_main函数或配置运行循环(使用CFRunLoopRef类型或NSRunLoop对象)的应用程序自动创建的。
  • 使用该dispatch_get_global_queue函数获取任何共享并发队列。

将任务添加到队列

要执行任务,您必须将其分派到适当的调度队列。可以同步或异步调度任务,可以单独调度,也可以成组调度。一旦进入队列,队列就负责尽快执行你的任务,考虑到它的约束和队列中已经存在的任务。

将单个任务添加到队列

有两种方法可以将任务添加到队列中:异步或同步。如果可能,使用dispatch_asyncdispatch_async_f函数的异步执行优先于同步替代方案。当您将块对象或函数添加到队列时,无法知道该代码何时执行。因此,异步添加块或函数可让您安排代码的执行并继续从调用线程执行其他工作。如果您从应用程序的主线程调度任务,这尤其重要——也许是为了响应某些用户事件。

尽管您应该尽可能以异步方式添加任务,但有时您仍可能需要同步添加任务以防止竞争条件或其他同步错误。在这些情况下,您可以使用dispatch_syncdispatch_sync_f函数将任务添加到队列中。这些函数会阻塞当前的执行线程,直到指定的任务完成执行。

重要提示: 您永远不应从在您计划传递给函数的同一队列中执行的任务调用dispatch_syncdispatch_sync_f函数。这对于保证死锁的串行队列尤其重要,但对于并发队列也应避免。

以下示例显示了如何使用基于块的变体以异步和同步方式分派任务:

dispatch_queue_t myCustomQueue;
myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL);
 
dispatch_async(myCustomQueue, ^{
    printf("Do some work here.\n");
});
 
printf("The first block may or may not have run.\n");
 
dispatch_sync(myCustomQueue, ^{
    printf("Do some more work here.\n");
});
printf("Both blocks have completed.\n");

任务完成时执行完成块

就其性质而言,分派到队列的任务独立于创建它们的代码运行。但是,当任务完成时,您的应用程序可能仍希望收到有关该事实的通知,以便它可以合并结果。对于传统的异步编程,您可以使用回调机制来实现这一点,但是对于调度队列,您可以使用完成块。

完成块只是您在原始任务结束时分派到队列的另一段代码。调用代码通常在启动任务时提供完成块作为参数。任务代码所要做的就是在完成工作时将指定的块或函数提交到指定的队列。

清单 3-4显示了使用实现的平均函数。平均函数的最后两个参数允许调用者指定在报告结果时使用的队列和块。在平均函数计算其值后,它将结果传递给指定的块并将其分派到队列中。为了防止队列过早释放,最初保留该队列并在调度完成块后释放它是至关重要的。

清单 3-4  在任务之后执行完成回调

void average_async(int *data, size_t len,
   dispatch_queue_t queue, void (^block)(int))
{
   // Retain the queue provided by the user to make
   // sure it does not disappear before the completion
   // block can be called.
   dispatch_retain(queue);
 
   // Do the work on the default concurrent queue and then
   // call the user-provided block with the results.
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      int avg = average(data, len);
      dispatch_async(queue, ^{ block(avg);});
 
      // Release the user-provided queue when done
      dispatch_release(queue);
   });
}

在主线程上执行任务

Grand Central Dispatch 提供了一个特殊的调度队列,您可以使用它在应用程序的主线程上执行任务。该队列为所有应用程序自动提供,并由在其主线程上设置运行循环(由CFRunLoopRef类型或NSRunLoop对象管理)的任何应用程序自动排空。如果您不是在创建 Cocoa 应用程序并且不想显式设置运行循环,则必须调用该dispatch_main函数来显式排空主调度队列。您仍然可以将任务添加到队列中,但如果您不调用此函数,这些任务将永远不会执行。

您可以通过调用该dispatch_get_main_queue函数来获取应用程序主线程的调度队列。添加到此队列的任务在主线程本身上串行执行。因此,您可以将此队列用作同步点,以便在应用程序的其他部分完成工作。

总结

GCD

Grand Central Dispatch, 纯C语言,提供了很多强大的函数。

优势

  • GCD是苹果公司为多核的并行运算提供的解决方案;
  • GCD会自动的使用CPU的更多核芯
  • GCD会自动管理线程的生命周期(创建线程、调度任务、线程销毁)
  • 我们开发中,只需要告诉GCD需要执行什么任务,并交给GCD去执行就可以了,并不需要去编写管理线程的代码。

使用

将任务添加到队列,并指定任务执行的函数

函数

任务内容使用 block 封装 任务的 block 没有参数,也没有返回值

dispatch_async 异步执行任务

  • 不用等待当前语句执行完毕,就可以执行下一条语句
  • 会开启线程执行 block 的任务
  • 异步是多线程的另一个称呼

dispatch_sync

  • 必须等待当前语句执行完毕,才会执行后面的语句
  • 不会开启线程
  • 在当前执行 block 的任务

队列

数据结构: 先进先出(FIFO) 先添加,先调度

队列.001.jpeg

同一时间,串行队列只能调度一个任务去执行;并发队列可以调度多个任务去执行。

主队列

  • dispatch_get_main_queue()
  • 专门用来在主线程上调度任务的串行队列;
  • 如果当前主线程正在执行任务,那么无论主队列中当前被添加了什么任务,都不会被调度。

全局并发队列

  • 为了方便我们的使用,苹果提供了全局队列 dispatch_get_global_queue(0, 0)
  • 全局队列是一个并发队列;
  • 使用多线程开发时,如果队列没有特殊要求,在执行异步任务时,可以直接使用全局队列。

函数与队列

同步函数串行队列

  • 不会开启新的线程,在当前线程执行任务;
  • 任务一个接着一个串行执行;
  • 会产生堵塞。

同步函数并发队列

  • 不会开启新的线程,在当前线程执行任务;
  • 任务一个接着一个执行。

异步函数串行队列

  • 开启一条新的线程;
  • 任务一个接着一个执行。

异步函数并发队列

  • 开启一条新的线程
  • 任务并发执行,顺序和CPU的调度有关。

验证

image.png

案例

耗时问题

队列的创建是一个耗时的操作

image.png

image.png

串行队列执行同步任务

image.png

串行队列执行异步任务

image.png

并发队列执行同步任务

image.png

并发队列执行异步任务

image.png

总结

  • 无论是串行还是并发队列发起同步任务还是异步任务,都会消耗一定的时间。
  • 发起异步执行任务相对可以节省一部分时间。

经典案例

并发队列的任务执行顺序

image.png

并发队列 queueT 添加任务之前, 1 先打印, 我们知道向队列中添加任务是一个耗时的操作,所以, 5 会在 2 之前 打印, 在2 之后, 是一个同步的操作,会阻塞当前任务的执行,所以,4 会在 3 之后打印。最终顺序是 1 - 5 -2 - 3 - 4;:验证:

image.png

串行队列的任务执行顺序

image.png

串行任务,会奔溃。为什么呢? 我们进行此问题的分析: 首先,队列换成的串行队列, 任务会一个一个的执行,首先5打印完了,打印2 ,这个没问题,我们同步函数添加到队列任务3,这就有问题了,会产生死锁

同步队列死锁分析.001.jpeg

任务3 添加会阻塞任务2 之后的代码执行,同时,因为串行队列只能一个一个多执行任务,所以任务3又在等待任务2多执行完毕。如此,死锁便产生了。

也就是 :_dispatch_sync_f_slow

image.png

结语

今天我们整理并总结了GCD的函数和队列相关的概念,并且结合实际的Demo对其概念的实际使用有所解释。下一篇,我们将一起深入到GCD源码到内部,探索其内部的实现原理。大家,加油!!!