美文网首页
Dispatch Queue

Dispatch Queue

作者: 渐z | 来源:发表于2018-03-16 10:02 被阅读24次

Grend Central Dispatch(GCD)调度队列是执行任务的强大工具。调度队列让我们可以与调用者异步或同步地执行任何代码块。可以使用调度队列来执行几乎所有用于在单独的线程上执行的任务。调度队列的优点是它们相应的线程代码更简单有效地执行这些任务。

本文提供了有关调度队列的介绍,以及有关如何使用它们在应用程序中执行常规任务的信息。如果想用调度队列替换现有的线程代码,可以从Migrating Away from Threads中找到有关如何执行此操作的一些其他提示。

关于调度队列

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

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

Type Description
Serial 串行队列(也称为私有调度队列)按照任务被添加到队列中顺序每次执行一个任务。当前正在执行的任务运行在由调度队列管理的不同线程上(可能因任务而异)。串行队列通常用于同步对特定资源的访问。
可以根据需要创建尽可能多的串行队列,并且每个队列都可以与其他队列同时运行。换句话说,如果创建了四个串行队列,每个队列只执行一个任务,但最多可以同时执行四个任务,每个队列一个。
Concurrent 并行队列(也称为全局调度队列)同时执行一个或多个任务,但任务仍按其添加到队列中的顺序启动。当前正在执行的任务在由调度队列管理的不同线程上运行。在任何给定点执行的任务的确切数量是可变的,并取决于系统条件。
在iOS 5及更高版本中,可以在自己创建调度队列时将队列类型指定为DISPATCH_QUEUE_CONCURRENT。另外,还有四个预定义的全局并发队列供应用程序使用。
Main dispatch queue 主调度队列是一个全局可用的串行队列,用于执行应用程序主线程上的任务。该队列与应用程序的 run loop(如果存在的话)一起工作,以将排队中的任务的执行与附加到 run loop 中的其他事件源的执行错开。因为它运行在应用程序的主线程上,所以主队列通常用作应用程序的关键同步点。

当向应用程序添加并发时,调度队列相对于线程提供了几个优点。最直接的优点是工作队列编程模型的简单性。使用线程,必须为要执行的工作以及创建和管理线程本身编写代码。调度队列让我们专注于我们实际想要执行的工作,而无需担心线程创建和管理。相反,系统会为我们处理所有的线程创建和管理。优点是系统能够比任何单个应用程序更有效地管理线程。系统可以根据可用资源和当前系统条件动态扩展线程数量。另外,相比我们自己创建线程,系统通常能够更快地开始运行任务。

为调度队列编写代码通常比为线程编写代码更容易,编写代码的关键是设计独立并且能够异步运行的任务。(这对于线程和调度队列都是如此。)但是调度队列具有优势的地方在于可预测性。如果有两个访问相同共享资源但在不同线程上运行的任务,则任一线程都可以先修改资源,并且需要使用锁来确保两个任务不会同时修改该资源。使用调度队列,可以将两个任务添加到串行调度队列,以确保在任何给定时间只有一个任务修改了资源。这种基于队列的同步比锁更有效,因为在有竞争和无竞争的情况下,锁始终需要昂贵的内核陷阱,而调度队列主要在应用程序的进程空间中工作,并且只在绝对有必要时调用内核。

虽然在串行队列中执行的任务不能同时执行,但必须记住,如果两个线程同时锁定,那么线程提供的任何并发会丢失或者显著减少。更重要的是,线程模型需要创建两个线程,它们同时占用内核和用户空间内存。调度队列不会为它们的线程支付相同的内存损失,并且它们使用的线程保持繁忙并且不会被阻塞。

有关调度队列的其他一些关键要点包括以下内容:

  • 调度队列相对于其他调度队列并行执行其任务。任务的序列化仅限于单个调度队列中的任务。
  • 系统确定任何时间点执行的任务总数。因此,有100个不同队列且每个队列有100个任务的应用程序可能不会并行执行所有这些任务(除非它具有100个或更多有效内核)。
  • 在选择启动哪些新任务时,系统会考虑队列优先级。
  • 队列中的任务在添加到队列时必须已准备好执行。(与Cocoa操作对象的使用不同)
  • 私有调度队列是被引用计数的对象。除了在自己的代码中保留队列之外,请注意,调度源也可以附加到队列中,并增加其引用计数。因此,必须确保所有调度源都被取消,并且所有retain调用均通过对应的release调用来保持平衡。有关引用和释放调度队列的更多信息,请参看调度队列的内存管理。有关调度源的更多信息,请参看Dispatch Source

与队列相关的技术

除了调度队列之外,Grand Central Dispatch还提供了几种使用队列来帮助管理代码的技术。下表列出了这些技术。

Technology Description
Dispatch group 调度组是一种监听一组block对象是否已完成执行的方法。(可以根据需要同步或异步监听block)组为代码提供有效的同步机制,这取决于其他任务的完成情况。
Dispatch semaphore 调度信号与传统信号相似,但通常更加高效。只有当调用线程因为信号量不可用而需要被阻塞时,调度信号才会调用内核。如果信号量可用,则不会调用内核。
Dispatch source 调度源生成通知来响应特定类型的系统事件。可以使用调度源来监听事件,例如进程通知,信号和描述符事件等。发送事件时,调度源将任务代码异步提交给调度队列进行处理。

创建和管理调度队列

在将任务添加到队列之前,必须确定要使用的队列类型以及打算如何使用它。调度队列可以串行或者并行执行任务。另外,如果有针对队列的特定用途,则可以相应地配置队列属性。以下各节介绍如何创建并配置调度队列以供使用。

获取全局并发调度队列

当有多个可以并行运行的任务时,并发调度队列非常有用。并发调度队列仍然是一个先进先出的队列,但是并发队列可能会在任何先加入的任务完成执行之前就执行后添加的任务。并发队列在任何给定时刻执行的实际任务数量都是可变的,并且可以随应用程序中的条件更改而动态更改。许多因素会影响并发队列执行的任务数量,包括可用内核数量,其他进程执行的工作量以及其他串行调度队列中的任务数量和优先级。

系统为每个应用程序提供四个并发调度队列。这些队列对于应用程序来说是全局的,并且仅通过它们的优先级来区分。因为它们是全局性的,所以不用明确地创建它们。而是使用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。

虽然调度队列是被引用计数的对象,但我们不需要对全局并发队列执行retainrelease操作。因为它们对应用程序是全局的,所以对这些队列执行retainrelease操作将被忽略。 因此,我们不需要存储对这些队列的引用,只需要在需要用的时候调用dispatch_get_global_queue函数来获取就行。

创建串行调度队列

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

与并发队列不同,必须明确创建并管理需要使用的任何串行队列。可以为应用程序创建任意数量的串行队列,但应该避免单独创建大量的串行队列,以便尽可能多地并行执行任务。如果想要并行执行大量任务,请将它们提交到某个全局并发队列。创建串行队列时,尝试确定每个队列的用途,例如保护资源或同步应用程序的某些关键行为。

以下代码显示了创建自定义串行队列所需的步骤。dispatch_queue_create函数有两个参数:队列名称和队列属性集合。调试器和性能工具显示队列名称,以帮助我们跟踪我们的任务如何执行。队列属性保留供将来使用,现在应传递NULL

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

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

在运行时获取通用队列

Grand Central Dispatch提供的功能允许我们从应用程序访问几个常见的调度队列:

  • 使用dispatch_get_current_queue函数进行调试或测试当前队列的标识。在block对象中调用该函数,该函数将返回block被提交到的队列(现在正在运行该队列)。在block外部调用此函数将返回应用程序的默认并发队列。
  • 使用dispatch_get_main_queue函数获取与应用程序主线程相关联的串行调度队列。此队列是为Cocoa应用程序以及调用dispatch_main函数或者在主线程上配置run loop(使用CFRunLoopRef类型或者NSRunLoop对象)的应用程序自动创建的。
  • 使用dispatch_get_global_queue函数来获取任何共享全局并发队列。

调度队列的内存管理

调度队列和其他调度对象是被引用计数的数据类型。创建串行调度队列时,其初始引用计数为1,可以使用dispatch_retaindispatch_release函数根据需要递增和递减引用计数。当队列的引用计数为零时,系统会异步释放队列。

保留和释放调度对象(如队列)以确保它们在使用时还保留在内存中很重要。与Cocoa对象的内存管理一样,一般规则是,如果打算使用我们创建的队列,则应在使用该队列之前保留该队列,并在不需要时释放它。这种基本模式可以确保只要使用队列,队列就会保留在内存中。

即使我们实现了一个垃圾回收应用程序,仍然必须保留并释放我们创建的调度队列和其他调度对象。Grand Central Dispatch不支持用于回收内存的垃圾回收模型。

使用队列存储自定义上下文信息

所有调度对象(包括调度队列)都允许我们将自定义上下文数据与对象相关联。要在给定的对象上设置和获取这些数据,可以使用dispatch_set_contextdispatch_get_context函数。系统不会以任何方式使用我们的自定义数据,并且由我们自己在适当的时间分配和销毁数据。

对于队列,我们可以使用上下文数据来存储指向Objective-C对象或其他数据结构的指针来帮助标识队列或者我们代码的预期用法。可以使用队列的finalizer函数(该函数已废弃)在队列被销毁之前销毁上下文数据。以下代码显示了如何编写一个清除队列的上下文数据的终结器函数的示例。

void myFinalizerFunction(void *context)
{
    MyDataContext* theData = (MyDataContext*)context;

    // Clean up the contents of the structure
    myCleanUpDataContextFunction(theData);

    // Now release the structure itself.
    free(theData);
}

dispatch_queue_t createMyQueue()
{
    MyDataContext*  data = (MyDataContext*) malloc(sizeof(MyDataContext));
    myInitializeDataContextFunction(data);

    // Create the queue and set the context data.
    dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL);
    dispatch_set_context(serialQueue, data);
    dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);

    return serialQueue;
}

将任务添加到队列

要执行一个任务,必须将其调度到合适的调度队列。可以同步或异步调度任务,并且可以单独或成组地调度它们。任务一旦进入队列,队列将负责尽快执行这些任务,为这些任务和已在队列中的任务添加约束。本节将介绍将任务调度到队列中的一些技术,并介绍每种技术的优点。

将单个任务添加到队列

有两种方法可以将任务添加到队列:同步或者异步。如果可能,使用dispatch_asyncdispatch_async_f函数的异步执行要优于同步。当我们将一个block对象或函数添加到队列中时,是无法得知该代码何时执行的。因此,通过异步添加block或者函数,可以调度代码的执行并继续从调用线程执行其他工作。如果我们正在应用程序的主线程安排任务(这可能是为了响应某些用户事件),这一点尤其重要。

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

重要:永远不要在和传递给dispatch_syncdispatch_sync_f函数的队列相同的队列中的正在执行的任务中调用dispatch_syncdispatch_sync_f函数。这对串行队列尤其重要,因为这样做会导致死锁,对于并发队列也要避免这样做。

以下代码显示了如何基于block来异步和同步调度任务:

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");

任务完成后执行Completion Block

就其本质而言,调度到队列中的任务独立于创建它们的代码运行。但是,当任务完成后,应用程序可能仍然需要通知该事实,以便它可以合并结果。使用传统的编程,可以使用回调机制来这样做,但对于调度队列,可以使用completion block。

completion block只是在原始任务结束时调度给队列的另一段代码。调用代码通常在其启动任务时提供completion block作为参数。所有任务代码所要做的就是在指定的队列完成其工作时,将指定的block或函数提交给指定的队列。

以下代码展示了使用block实现的计算平均数的函数。计算平均数函数的最后两个参数允许调用者报告结果时指定一个队列和block。计算平均数函数在计算出结构后,将结果传递给指定的block并将其调度到队列中。为了防止队列被过早释放,首先保留该队列并在completion block被调度后释放它是至关重要的。

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);
    });
}

并行执行循环迭代

在循环执行固定迭代次数的地方,使用并发调度队列可能会提高性能。例如,假设有一个for循环,通过每个循环迭代完成一些工作:

for (i = 0; i < count; i++) {
    printf("%u\n",i);
}

如果在每次迭代执行期间执行的工作与所有其他迭代期间执行的工作不同,并且每个后续循环完成的顺序不重要,则可以使用dispatch_apply或者dispatch_apply_f函数调用来替代循环。这些函数为每个循环迭代提交指定的block或函数到一个队列中。当调度到并发队列时,可以并行执行多个循环迭代。

调用dispatch_apply或者dispatch_apply_f函数时可以指定一个串行队列或一个并行队列。传入并行队列允许我们同时执行多个循环迭代,并且是使用这些函数的最常见方式。虽然也允许使用串行队列,但这相对于使用循环并没有真正的性能优势。

重要:与常规for循环一个,dispatch_apply或者dispatch_apply_f函数在所有循环迭代完成之后才会返回。因此,在从正在队列的上下文中执行的代码中调用它们时要小心。如果作为参数传递给函数的队列是串行队列,并且与执行当前代码的队列相同,则调用这些函数将导致队列死锁。因为它们会阻塞当前线程,使事件处理循环无法及时响应事件,所以在主线程调用这些函数时应该小心。如果循环代码需要大量的处理时间,则可能需要从不同的线程调用这些函数。

以下代码显示了如何使用dispatch_apply函数替代前面的for循环。传递给dispatch_apply函数的block必须包含一个标识当前循环迭代的参数。在执行该block时,此参数的值在第一次迭代中为0,在第二次中为1,依此类推。最后一次迭代的参数值时count-1,其中count时迭代的总次数。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(count, queue, ^(size_t i) {
    printf("%u\n",i);
});

应该确保任务代码在每次迭代中都会做一些合理的工作。与任何调度到队列的block或函数一样,调度该代码以供执行也会有开销。如果循环的每次迭代执行少量工作,则调度代码的开销可能会超过将其调度到队列中可能带来的性能提升。如果在测试过程中发现这是真的,则可以使用跨越来增加每次循环迭代期间执行的工作量。通过跨越,可以将原始循环的多个迭代组合到一个block中,并按比例减少迭代次数。例如,如果最初执行100次迭代,但决定使用4次跨越,则现在在每个block中执行4次循环迭代,并且迭代次数变为25次。有关如何实现跨越的示例,请参看改进循环代码

在主线程中执行任务

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

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

在任务中使用Objective-C对象

GCD为Cocoa内存管理技术提供了内置支持,因此可以在提交到调度队列的block中自由使用Objective-C对象。每个调度队列维护自己的自动释放池,以确保自动释放分对象在某个时刻被释放。队列无法保证在何时实际释放这些对象。

如果应用程序的内存受限并且block创建了多个自动释放对象,则创建我们自己的自动释放池是确保及时释放对象的唯一方法。如果block创建了数百个对象,则可能需要创建多个自动释放池或者定期排空自动释放池。

有关自动释放池和Objective-C内存管理的更多信息,请参看Advanced Memory Management Programming Guide

暂停和恢复队列

可以通过挂起队列暂时阻止其执行block对象。使用dispatch_suspend函数暂停调度队列,并使用dispatch_resume函数恢复它。调用dispatch_suspend函数会使队列的暂停引用计数加1,调用dispatch_resume会使队列的暂停引用计数减1。当暂停引用计数大于零时,队列保持挂起状态。因此必须保持dispatch_suspend函数的调用与dispatch_resume函数的调用平衡,以便恢复处理block。

重要提示:暂停和恢复的调用式异步的,暂停队列不会导致正在执行的block停止执行。

使用调度信号来调节有限资源的使用

如果提交给调度队列的任务访问某些有限的资源,则可能需要使用调度信号来调节同时访问该资源的任务数量。调度信号像常规信号一样工作,只有一个例外。当资源可用时,获取调度信号比获取传统信号需要的时间更少。这是因为Grand Central Dispatch不会为这种特定情况去调用内核。只有在资源不可用并且系统需要停止线程直到发出信号为止时,才会调用系统内核。

使用调度信号的语义如下:

  • 当创建信号量时(使用dispatch_semaphore_create函数),可以指定一个指示可用资源数量的正整数。
  • 在每个任务中,调用dispatch_semaphore_wait函数来等待信号。
  • dispatch_semaphore_wait函数调用返回时,获取资源并完成要执行的工作。
  • 当完成工作后,释放资源并调用dispatch_semaphore_signal函数发出信号。

有关这些步骤如何工作的示例,请考虑使用系统中的描述文件符。每个应用程序都使用有限数量的文件描述符。如果我们有一个处理大量文件的任务,我们不希望一次打开太多的文件以至于用光文件描述符。相反,我们可以使用信号量来限制文件处理代码一次使用的文件描述符的数量。如下所示:

// Create the semaphore, specifying the initial pool size
dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);

// Wait for a free file descriptor
dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
fd = open("/etc/services", O_RDONLY);

// Release the file descriptor when done
close(fd);
dispatch_semaphore_signal(fd_sema);

在创建信号量时,可以指定可用资源的数量。该值将成为信号量的初始计数变量。每次在信号量上等待时,dispatch_semaphore_wait函数会将该变量的计数减1.如果结果值为负数,该函数会通知内核阻塞当前线程。另一方面,dispatch_semaphore_signal函数将计数变量加1,表示资源已被释放。如果有任务被阻塞并等待资源,它们中的一个随后会被解除阻塞并允许其工作。

等待排队任务组

调度组是阻塞线程直到一个或多个任务完成执行的一种方式。例如,在调度几个任务来计算一些数据之后,可以使用一个组来等待这些任务,然后在它们都完成时处理结果。另一种使用调度组的方式是作为线程连接的替代方法。可以将相应的任务添加到一个调度组然后等待整个组,而不是启动多个子线程并将每个任务加入其中一个线程。

以下代码显示了设置一个组并调度任务给它,然后等待结果的基本过程。不是使用dispatch_async函数将任务调度到队列,而是使用dispatch_group_async函数将任务与组相关联并队列执行。要等待一组任务完成,可以使用dispatch_group_wait函数传递相应的组。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

// Add a task to the group
dispatch_group_async(group, queue, ^{
    // Some asynchronous work
});

// Do some other work while the tasks execute.

// When you cannot make any more forward progress,
// wait on the group to block the current thread.
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

// Release the group when it is no longer needed.
dispatch_release(group);

调度队列和线程安全

在调度队列中讨论线程安全可能看起来很奇怪,但线程安全仍然是一个相关主题。任何时候在应用程序中实现并发时,都应该知道以下几件事情:

  • 调度队列本身是线程安全的。换句话说,我们可以将任务从系统中的任何线程提交到调度队列,而无需首先获取锁或者同步访问队列。
  • 不要从传递给dispatch_sync函数的同一队列中执行的任务中调用dispatch_sync函数。这样做会导致队列死锁。如果需要调度到当前队列,请使用dispatch_async函数异步执行。
  • 避免从提交给调度队列的任务中获取锁。虽然使用来自任务的锁是安全的,但是当我们获取锁时,如果该锁不可用,则可能会完全阻塞串行队列。同样,对于并发队列,等待锁可能会阻止执行其他任务。如果需要同步部分代码,请使用串行调度队列而不是锁。
  • 尽管我们可以获取有关运行任务的基础线程的信息,但最好避免这样做。有关调度队列与线程的兼容性的更多信息,请参看Compatibility with POSIX Threads

有关如何将现有线程代码更改为使用调度队列的其他提示,请参看Migrating Away from Threads

相关文章

网友评论

      本文标题:Dispatch Queue

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