iOS并发编程—Dispatch Queues

翻译自Apple官方文档,是时候表演翻译腔的技术了
dispatch queue用得是足够多了,但是从来没有系统深入地了解过

概述

Grand Central Dispatch(全局集中派遣,简称GCD),是一个执行任务的强大工具。Dispatch queues(派遣队列)能够让你同步或者异步执行任意的代码块——与调用者之间。你能够使用Dispatch queues来执行几乎所有你在单独的线程中执行的操作。dispatch queues的优点就是,它们能够更加简单地被使用,并且对比起执行相同任务的线程,它们更加高效。
本章节将介绍dispatch queues,还有如何使用它们来执行普通的任务。如果你想将现有的使用线程的代码转化为使用dispatch queues,你可以在这里找到额外的帮助。

关于Dispatch Queues

Dispatch queues 是一种执行异步跟并发任务的简单方法。“任务”是一些你的程序中需要执行的操作。例如,你可以将这些当做一个任务:一些运算、创建或者修改数据结构、某些文件数据的读取等等。要定义一个任务,你可以将相关的代码放到一个函数中,或者放到一个block中,然后将它添加到dispatch queue里面。
一个dispatch queue是一个类似对象的结构,管理着你提交给它的任务。所有dispatch queue都是fifo(先进先出)的数据结构。这样,你添加到队列中的任务通常会以相同的顺序来开始执行。虽然GCD自动为你提供了一些dispatch queues,但你也能自己创建一些另外的来执行某些特殊任务。下表列出了可用的dispatch queues的类型。

Type Description
Serial(序列) serial 队列(也就是人们所熟知的私有dispatch queue)同一时间执行一个任务——按照任务被添加到队列中的顺序。现在正在执行的任务在一个另外的,被dispatch queue所管理的线程中运行(这个线程会因任务的不同而不同)。serial队列通常被用来同步地使用某些特殊的资源。
你可以根据你的需要创建任意数量的serial队列,并且每一个队列跟其他队列之间是并发执行的。换句话说,如果你创建了四个serial队列,每一个队列同一时间只执行一个任务,但是仍然有多达四个任务能够并发执行,每一个任务都来自自己所属于的队列。如何创建Serial Dispatch Queues
Concurrent(并发) 并发队列(最为被广泛了解的全局派遣队列—global dispatch queue)并发地执行一个或者多个任务,但是任务的开始运行的顺序仍然跟它们被添加到序列的顺序相同。当前正在执行的任务在一个单独的被dispatch queue管理的线程中运行。任意时刻中,正在执行的任务的数目都不尽相同,并且依赖于系统条件。
在iOS5或更新的系统中,你可以自己创建并发的dispatch queue——使用DISPATCH-QUEUE-CONCURRENT作为队列类型(queue type)。另外,一共有四种预先定义的全局并发队列可用。如何获取全局并发队列
Main dispatch queue(主派遣队列) 主派遣队列是一个全局的serial队列,它在app的主线程中执行任务。这个队列利用app的运行周期来工作——将队列中任务的执行插入到正在app运行周期中执行的事件之间。因为main dispatch queue在app的主线程中运行,它通常被用做app的关键同步点,来同步一些数据。
虽然你并不需要创建main dispatch queue,但你需要确定你的程序正确的排空它。戳这在主线程中执行任务

当你需要在程序中加入并发,对比起使用thread,使用dispatch queues有几个优势。最直观的优势就是更加简便的编程模式。如果你使用threads的话,你需要编写你需要执行的代码,还有创建以及管理线程的代码。dispatch queue让你专注于你实际需要执行的任务中,而不用去担心线程的创建以及管理。相反,系统会为你处理所有的线程创建以及管理。好处就是,对比起一个app能够做的,系统往往能够更加有效的管理线程。系统可以基于可用资源以及当前的系统状态动态地调整线程的数目。另外,系统通常可以更快地开始运行你的任务,对比起你自己手动创建线程。

虽然你可能会认为将自己的代码重写为使用dispatch queue的模式会很费劲,但实际上比起使用thread,它会简单多了。编写代码的关键就是设计独立的、能够异步运行的任务(实际上对于使用thread或者使用dispatch queue都同样重要)。然而,dispatch queue所拥有的优势是显而易见的。如果已有两个任务,并且他们都会访问同一共享的资源,但是在不同的线程中运行。而且任一一个线程都可以先修改数据,而你因此需要使用lock来确保两个任务不会在同一时间内修改数据。有了dispatch queues,你可以将两个任务都添加到serial dispatch queue中,来确保在给定的时间点只有一个任务修改数据。这种基于队列的同步操作,比lock更加高效。因为lock通常需要昂贵的内核触发机制(kernel trap),无论是竞争还是非竞争的情况中。然而,dispatch queue主要在app的进程空间里工作并且只会在绝对需要的时候才会深入到调用kernel层的资源。

虽然你可能会马上指出,两个在serial queue中运行的任务并不能并发地执行(对啊,我刚想说,苹果知我心),但是你需要记住:如果两个线程在同一时间内被锁住,任何由线程带来的并发性能都会丢失或者显著降低。更重要的是,使用线程的工作模式需要创建两个线程,这会耗用系统内存以及用户空间内存。dispatch queues并不会承受相同的内存耗费,并且它们使用的线程会一直保持busy,不会发生阻塞。

下面是关于dispatch queues的一些需要铭记于心的重点:

  • 不同的Dispatch queues 之间是并发地执行它们各自的任务的。执行的任务的序列长度受限于单个dipatch queue中的任务数目。
  • 系统决定了同一时间内能够执行的任务的总数。因此,如果在一个app中,有100个任务,分别在100个不同的队列中,它们可能不会同时被并发地执行(除非系统有100个核或者它的核很高效)。
  • 系统在选择哪个新任务来开始执行的时候,会将队列的优先级别也纳入考虑。戳这查看如何设置队列的优先级
  • 被添加到队列中的任务,在它们被添加的时候,必须处于准备被执行的状态(如果你之前曾经使用过Cocoa的operation对象,注意这里跟使用operation的模式中所做的不一样)。
  • 私有dispatch queues是引用计数(reference-counted)对象。除了需要在自己的代码中retain队列,需要注意的是dispatch source同样能被attach到队列中,因而同样需要增加它们的引用计数。因此,你需要保证,所有dispatch sources都被cancel并且所有retain都有对等的release对应。戳这查看如何retain跟release队列,戳这查看什么是dispatch sources

想要知道更多关于用来操控dispatch queues的接口的信息,戳这里Grand Central Dispatch Reference

Queue相关的技术

除了dispatch queue,Grand Central Dispatch提供了一些技术,可以让你使用队列来帮助管理自己的代码。下表列出了这些屌屌的技能,还有你可以找到它们更多信息的links。

Technology Description
Dispatch groups(派遣组) dispatch group是一种监听一组completion block对象的方法。你可以同步或者异步监听block。goups为那些需要依赖于任务执行完毕的代码提供了一种有效的同步机制。戳这了解WaitingonGroups of Queued Tasks
Dispatch semaphores(派遣信号) dispatch semaphore跟传统的semaphore类似,但是通常来说更加高效。dispatch semaphores 只有在调用的线程由于semaphore不可用而被阻塞时,才会深入到kernel层。如果semaphore可用,不会有任何kernel层的调用。戳这可以看例子使用dispatch semaphores来控制使用有限的资源
Dispatch sources(派遣资源) dispatch source会生成notificaton来回应特定类型的系统事件。你可以使用dispatch source来监听事件,例如process notification,signals,还有其他dispatch source的descriptor事件。当事件发生时,dispatch source会异步地将你的任务代码提交到指定的dispatch queue中来执行。戳这查看更多有关如何[创建以及使用dispatch source][dispatch source]。

使用Block来实现业务代码

Block对象是基于C语言实现的,你可以使用C,Objective-C还有C++代码来使用Block。Block让定义独立的业务单元变得容易。尽管它们看起来很像函数指针,但是block实际上是由底层的数据结构来表示,类似于对象,并且是由编译器为你生成并且管理的。编译器会将你提供的代码打包(跟相关的数据一起),并且以一种能够在堆中存在和能够传递给你的app的形式将它包含起来。

block的一个非常屌的优势就在于它们能够使用在它们语法范围外的变量。当你定义在一个函数或者方法里面定义了一个block,block就会在某些方面会像传统的代码块一样执行。例如,一个block可以读取在父级代码块中定义的变量的值。block读取的变量会被复制到block位于堆的数据结构中,因此block可以在稍候访问它们。当block被添加到dispatch queue,这些值必须以一种只读的格式存留。然而,那些同步执行的block也可以使用那些拥有__block关键字的变量。

在代码内声明block所使用的符号跟函数指针使用的符号相同。主要的不同在于block的名字跟在^的后面而不是*。跟函数指针类似,你可以传入一些参数到block中,并且能够接收它的返回值。下面的代码展示怎样来声明跟同步执行你的block。

  • 变量aBlock被声明为一个block,接收一个整型参数,并且不返回值
  • 一个aBlock的类型相匹配的block随后被声明并且分配给aBlock
  • 最后一行立即执行这个block,打印那个指定的整数。
1
2
3
4
5
6
7
8
9
10
int x = 123;
int y = 456;

// Block declaration and assignment
void (^aBlcok)(int) = ^(int z) {
printf("%d %d %d\n", x, y, z);
};

// Execute the block
aBlock(789); //prints: 123 456 789

下面列出了一些你应该在设计block时应该考虑的重点

  • 对于那些你打算用dispatch queue来异步执行的block,只读取父级代码块中的变量是安全的。然而,你不应该试图读取庞大的结构或者其他在调用上下文中被分配或删除的基于指针的变量。因为当你的block执行时,那些被指针引用的内存可能已经被释放掉了。当然,如果自己手动分配内存(或者一个对象)并且明确地释放block的内存,那便是安全的。
  • Dispatch queue会复制那些添加到它们中的block,并且当它们执行结束之后会释放持有的block。换句话说,你不需要手动将block复制到队列中。
  • 对比起原始的线程手段,队列在处理起某些小人物的时候更加高效,经常性的做法是创建一些block来在队列中执行他们。如果一个block执行的任务规模太小,可能直接在普通代码中执行它们开销会更小。分辨一个block的任务规模是否过小的办法,就是用performance tools来测试对比在队列以及在普通代码中的执行开销。
  • 不要试图获取跟底层线程有关的数据,也不要试图在其他block中读取它。如果同一个队列中的多个任务想要共享数据,应该使用dispatch queue的上下文指针(context pointer)来存储数据。戳这里查看如何访问dispatch queue的上下文数据
  • 如果你的block想要创建不少oc对象,可能你会想要将你的block中的部分代码包含在@autorelease块里面来解决内存管理问题。虽然GCD dispatch queues拥有它们自己的autorelease pool,但是它们并不保证何时会将那些pool清空。如果你的app有内存限制,创建你自己的autorelease pool能够让你更规律地释放你的autoreleased object的内存。

创建并管理dispatch queues

在你将任务添加到队列之前,你应该决定需要使用何种队列还有如何使用它。dispatch queue可以按顺序地执行任务或者并发地执行任务。另外,如果你对队列有特殊用途,你可以根据需要设置队列的属性。下面的章节介绍如何创建队列并且设置它们。

获取全局的concurrent dispatch queue

如果你想要多个任务并行运行,那么并发的dispatch queue是你的首选。一个并发的队列仍然会按照先进先出的顺序执行任务,只不过,并发队列可能会在前面的任务执行完成之前就开是下一个新任务的执行。在给定的时间点,并发队列执行的实际任务数量会随app的状态变化。很多因素都会影响并发队列执行任务的数目,包括可用的运算核心,其他进程的运行规模,还有其他dispatch queue的优先级。

系统为每个app提供了四种并发队列。这些队列都是在app中全局可访问的,并且他们仅仅被根据优先级别来区分。由于他们是全局的,你不显式创建他们。相反,你应该用dispatch_get_global_queue函数来获取它们,如下:

1
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

除了获取默认并发队列之外,你同样可以通过传入不同的参数获取高优先级或者低优先级的队列:DISPATCH_QUEUE_PRIORITY_HIGH 或者 DISPATCH_QUEUE_PRIORITY_LOW,或者通过常量DISPATCH_QUEUE_PRIORITY_BACKGROUND来获取后台队列。正如你所想,高优先级的队列会比默认优先级以及低优先级的队列先被执行。同样地,默认优先级的队列会比低优先级的队列先被执行。

注意:dispatch_get_global_queue函数里面的第二个参数是保留参数,将来有可能使用。而现在,你传入0就可以了。

虽然dispatch queue是引用计数对象,但是你并不用对全局并发队列进行显式的retain或者release。因为他们在你的app中全局存在,对这些队列的retain或者release都会被忽略。因此,你不用存储这些队列的引用。任何时候你需要使用他们,你只需要通过dispatch_get_global_queue函数。

创建serial dispatch queue

如果你想要你的任务以一个特定的顺序来执行,那么你应该使用serial queue。一个serial queue在同一时间内之运行一个任务,并且通常在队列的头部取出任务。你可以使用serial queue而不是线程锁来保护共享资源或者可被修改的数据结构。跟线程锁不一样,同步的队列确保了所有任务会以一个预先定好的顺序来执行。并且只要你异步地将任务提交到serial queue中,队列永远不会发生死锁。

跟concurrent queue不同。concurrent queue是已经创建好的,不用你操心。而serial queue需要你手动创建并且管理所有serial queue。你可以创建任意数量的serial queue,但是应该避免创建一个庞大数目的serial queue来为了尽可能多地同时执行任务。如果你想要并发的执行大量任务,应该将他们提交到全局的并发队列。当你创建serial queue,应该尝试区分每个队列的工作意图。例如是为了保护资源或者是让app中的一些关键操作同步执行。

下表展示创建自定义的serial queue所需要的步骤。dispatch_queue_create函数需要两个参数:queue的名字还有queue的一组属性。调试器跟performace tool会展示队列的名字来帮助你跟踪你的任务是如何被执行的。queue的属性这个参数是为将来保留的,现在应该传入NULL。

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

除了你自己手动创建的队列之外,系统自动创建了一个serial queue并将它绑定到主线程中。

在运行时获取公共的队列(common queues at runtime)

GCD提供了一些函数来让你访问一些公共的dispatch queue:

  • dispatch_get_current_queue:当你需要调试或者测试当前队列的唯一标识,可以调用这个函数。在一个block对象中调用这个函数,会返回这个block被提交的队列(同时也是这个block正在运行所在的队列)。如果在一个block之外调用这个函数,将返回app中的默认concurrent queue。
  • dispatch_get_main:获取与app主线程所关联的serial dispatch queue。这个队列是为cocoa应用自动创建的。另外在一些应用中,如果他们调用了dispatch_main函数,或者在主线程使用CFRunLoopRef类型或NSRunLoop对象来配置过运行周期,那么这个队列也会被自动创建。
  • dispatch_get_global_queue:可以获取任意的共享的concurrent queue。
dispatch queue的内存管理

dispatch queue还有其他的dispatch object是引用计数的数据类型。当你创建了一个serial dispatch queue,它会有一个初始的引用计数 1。你可以使用dispatch_retaindispatch_release函数来增加或者减少引用计数。当队列的引用计数达到了0,系统会异步释放队列。

retain或者release dispatch object很重要,例如队列,确保他们在被使用的时候仍然留存在内存中。跟对cocoa object的手动内存管理一样,基本规则是:如果你想要使用一个被传入到代码中的队列,你应该在你使用它之前retain它,在不再需要它的时候release它。这个基本模式确保了只要你正在使用它,它就会留存在队列中。

注意:你不需要对任何一个global dispatch queue执行rretain或者release操作,包括concurrent dispatch 队列或者main dispatch queue。任何对这些队列的retain以及release操作都会被忽略。

即使你实现了一个拥有垃圾回收(garbage-collected)机制的应用,你仍然需要retain以及release你的dispatch queue以及其他dispatch object。GCD并不支持garbage collection模型来回收内存。

在队列中存储自定义上下文信息(custom context information)

所有dispatch object(包括dispatch queue)允许你通过对象来关联一些custom context data。要设置或者获取在给定对象中的custom context information,你可以使用dispatch_set_context或者dispatch_get_context函数。系统不会通过任何方式使用你的自定义数据,在适当的市价分配或者释放自定义数据完全取决于你。

对队列来说,你可以使用context data来存储Objective-C对象的指针,或者是其他的用来识别队列的数据结构,又或者是你打算在代码中使用的数据结构。你可以使用队列的析构函数(finalizer)来释放(或者取消关联)你的队列中的context data——在队列被释放之前。下面有个例子,展示如何编写析构函数来清除队列的context data。

提供队列的清理函数

在你创建了serial dispatch queue之后,你可以将finalizer function附加到队列中,来执行在队列被释放的时候做一些自定义的清理操作。dispatch queue是引用计数对象,你可以使用dispatch_set_finalizer_f函数来制定一个函数,在队列的引用计数到达0的时候被调用。你可以使用这个清理函数来清除队列关联的context data,并且只有当context pointer不为NULL的时候,这个函数才会被调用。

下面将会向你展示一个自定义的finalizer function,还有一个创建并且设置finalizer的函数。那个队列将使用finalizer function来释放存储在队列的context pointer中的数据。(那个myInitializeDataContextFunctionmyCleanUpDataContextFunction函数是自定义的函数,是你用来初始化还有清理那个数据结构的内容的。)传入到finalizer function中的context pointer包含关联到队列中的数据对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void myFinalizerFunction(void *context) {
MyDataContext* theData = (MyDataContext*)context;

// 清理结构中的数据
myCleanUpDataContextFunction(theData);

// 现在释放这个结构本身
free(theData);
}

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

// 创建队列并且设置context data
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CritialTaskQueue", NULL);
if (serialQueue) {
dispatch_set_context(serialQueue, data);
dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);
}

return serialQueue;
}

将任务添加到队列中

要执行任务,你需要将它分配到合适的dispatch queue中。你可以同步地或者异步地分配任务,同时你也可以将他们单个分配或者秤组分配。当队列的限制条件已经给定,并且任务已经被分配到队列中,那么队列就会负责尽可能快地执行你的任务。这个章节会展示几个分配任务的技术,还有阐述每种方法的优势。

将单个任务添加到队列中

将一个任务添加到队列中有两种方法:同步或者异步。可以的话,尽量使用异步的方式(通过dispatch_asyncdispatch_async_f函数)比使用同步的方式更好。当你将一个block或者函数添加到队列中,并没有任何途径可以知道代码什么时候会被执行。因此,以一步的方式将block或者函数添加到队列中让你安排了任务的执行的同时,可以继续进行剩下的其他操作。如果你正在app的主线程执行任务,这将非常重要——例如正在响应用户的某些事件。

虽然你应该尽可能地异步添加任务,但是你仍然可以在需要的时候同步地添加任务来避免产生竞争状况(race conditions)或者其他的同步错误。在这些情况中,你可以使用dispatch_syncdispatch_sync_f函数来将任务添加到队列。这些任务将阻塞当前线程,直到任务完成执行。

重要:如果某个任务正在某个队列中执行,并且你的任务中打算使用dispatch_sync或者dispatch_sync_f函数,那么请永远不要将这个队列传入到前面所说的两个函数中。这对serial来说很重要,因为这肯定会引发死锁。在并发队列中也同样需要避免。

下面的例子展示了如何使用基于block的变体来同步或者异步分配任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dispatch_queue_t myCustomQueue;
myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL);

dispatch_async(myCustomeQueue, ^{
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

本质上说,被添加到队列中的任务,与创建这些任务的代码之间,是独立运行的。然而,当任务执行完成,可能你的app想要获得相应的通知,因而获取到相应的结果。传统的异步编程中,你可能会通过回调(callback)机制来完成,但在dispatch queue中,你可以通过completion block来实现。

一个completion block只是另外一片代码,你用来分配到队列中,安排在你原来的任务的结尾。代码在开始那个任务的时候,将completion block当成一个参数传入。

下表展示了一个averaging function,它用block来实现。剩下的两个参数允许调用者指定一个队列还有block,来返回结果。在averaging函数计算完他的值,他将结果传入到给定的block中,并且将block分配到队列中。为了阻止队列被提前release掉,有一些重要的地方要注意:在开始的地方retain队列,并且在completion block被派遣之后release队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void average_async(int *data, size_t len, dispatch_queue_t queue, void(^block)(int))
{
// 将用户提供的队列retain
// 确保他在完成之前不会消失
// 调用block
dispatch_retain(queue);

// 在默认的并发队列中执行操作
// 然后调用用户提供的block,并传入结果
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
int avg = average(data, len);
dispatch_async(queue, ^{block(avg);});

// 当完成了之后释放用户提供的队列
dispatch_release(queue);
});
}
并发地运行循环

concurrent dispatch queues可能可以让你代码中的某些部分提高性能——在你有一个执行固定次数的循环的地方。例如,假设你的一个循环在每次执行都会做一些东西:

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

如果每次循环之内做的事情跟其他循环之间都不一样,并且每个循环完成的先后顺序没有关系,那么你可以将循环替换成dispatch_apply或者dispatch_apply_f函数。这些函数将给定的block或者函数提交到队列中,并且是每次循环提交一次。当被提交到并发的队列,一个循环内的多个不同次数的循环就可以同时被执行。

当调用dispatch_apply或者dispatch_apply_f的时候,你可以指定用serial queue 或者concurrent queue。使用concurrent queue允许你同时执行多个不同次数的循环,并且这也是最常用的方法。虽然使用serial queue也是允许的,并且在代码上也是正确的,但是使用这样的队列对比起原来的普通循环代码并没有实际的性能提升。

重要:正如普通的循环一样,dispatch_applydispatch_apply_f函数直到所有循环都完成之后才会返回。所以你应该注意,当在代码中调用他们的时候,调用的代码是否已经被安放在一个队列中运行着。如果你传入到dispatch_apply或者dispatch_apply_f函数中的队列是serial queue,并且跟当前代码所在的队列是同一个,那么调用这两个函数就会发生死锁。
因为他们实际上会阻塞当前线程,当在主线程调用这些函数的时候,同样要小心,因为他们有可能会让你没有办法及时地处理你的事件(event)。如果你的循环代码需要显著的运行时间,那么你应当在其他线程调用这些函数。

下面的代码将展示如何将之前的循环结构替换成dispatch_apply函数。传入到dispatch_apply中的block必须包含一个单独的参数,来标识当前的循环迭代。当block被执行,这个参数的值为0代表第一次迭代,1表示第二次等等。最后一次迭代的参数值是count - 1,其中count是整个循环的次数。

1
2
3
4
5
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或者函数,都会有一些固有的额外开销,来安排那些代码的执行。如果在循环的每个迭代中执行仅仅是小规模的操作,那些固有的用于安排代码执行的开销可能会比你用这个结构得到的好处要大,得不偿失。如果你在测试的时候发现前面所说的情况出现了,那么你可以使用striding来增大每次循环中要执行的操作的规模。使用了striding之后,你可以将原始循环中的多个迭代编成一个组,并且将这个组放到一个block中,来相应地降低迭代的次数。例如,如果你最开始要执行100次迭代,但是打算使用4个stride,那么你现在就会在每个block执行4个循环迭代并且你的总迭代次数是25。戳这里看例子

在主线程中执行任务

GCD提供了一个特殊的dispatch queue,你可以用来在你的app主线程中执行任务。这个队列在所有app中都会自动生成,并且会被那些在主线程建立了运行循环(被CFRunLoopRef或者NSRunLoop对象管理)的app自动排空(指的是执行队列中的任务)。如果你创建的不是cocoaapp,并且不想显式建立run loop,你需要调用dispatch_main函数来显式排空main dispatch queue。你可以将任务添加到队列中,但是如果你不调用这个函数(dispatch_main),那么里面的任务将永远不会被执行。

你可以通过dispatch_get_main_queue函数来获取app中主线程相关联的dispatch queue。被添加到队列中的任务将会在主线程中被顺序执行。因此,你可以使用这个队列,来当做是你app中其他正在被执行的任务的同步点。

在任务中使用Objective-C对象

GCD内置了一种支持coco内存管理的机的技术,因而你可以在你提交到dispatch queue的block中自由地使用Objective-C对象。每个dispatch queue会维持一个autorelease pool,来确保autoreleased object能够在某些时刻被释放。队列并不保证他们实际上什么时候释放那些对象。

如果你的app是受内存制约的,并且你需要创建不少block,那么创建你自己的autorelease pool是的能够确保你的对象能在适当的时间被释放的唯一途径。如果你的block创建上百个对象,你可能需要创建不止一个autorelease pool来以定期地排空你的pool。

想知道更多的关于autorelease pools以及Objective-C内存管理的知识,可以查看Advanced Memory Management Programming Guide。

暂停与恢复队列

你可以将队列暂停,来让他临时停止执行他的block。暂停队列可以使用dispatch_suspend函数,而恢复它可以使用dispatch_resume函数。调用dispatch_suspend会增加队列的“暂停引用计数”,而调用dispatch_resume会减少计数。当引用计数大于0,队列就会保持暂停状态。因此,你需要平衡所有suspend调用以及resume调用。

重要:suspend还有resume这两个调用是异步地,并且仅仅在多个block的执行间隔之间生效。也就是说,暂停一个队列并不会将正在执行的block停下来。

使用dispatch semaphores来控制有限资源的使用

如果你提交到dispatch queue中的任务会访问一些有限的资源,那么你可能想要使用dispatch semaphore来控制同时访问那些资源的任务数目。dispatch semaphore工作的机制类似于普通的带有exception信息的semaphore。当资源可用,获得dispatch semaphore的时间会比传统的获得system semaphore的时间要短。这是因为GCD并不会在这种情况下不会深入到kernel进行调用。只有当资源不可用了,并且系统需要将你的线程暂停下来直到semaphore被发出,GCD才会深入到kernel进行调用。

使用dispatch semaphore的语法规则如下:

  1. 当你创建semaphore(使用dispatch_semaphore_create函数),你可以指定一个正整数来表示可用的资源总数。
  2. 在每个任务中,调用dispatch_semaphore_wait来等待semaphore。
  3. 当上述的等待函数返回之后,获取资源并执行你的任务。
  4. 当你使用资源结束之后,释放资源并且发出semaphore,通过调用dispatch_semaphore_signal函数。

举个例子,考虑使用系统的文件描述符(file descriptor)的情况。每个app被给定了固定数量的file descriptor。如果你有一个任务,需要访问大量的文件,你并不想要同一时间之内打开非常多的文件,来让你将所有文件描述符用完。那么,你可以是使用semaphore来显示file descriptor的同时使用数量。代码的基本部分如下:

1
2
3
4
5
6
7
8
9
10
// 创建semaphore,指定最初的pool size
dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);

// 等待可用的file descriptor
dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
fd = open("/etc/services", O_RDONLY);

// 业务完成之后释放文件描述符
close(fd);
dispatch_semaphore_signal(fd_sema);

当你创建了semaphore,你要指定可用的资源的数量。这个值将会是初始semaphore的可用数量。每一次你等待semaphore,dispatch_semaphore_wait函数会将可用数量减1。如果减1之后的结果是负数,那么函数就会告诉kernel来讲你的线程阻塞。另一方面dispatch_semaphore_signal函数会将可用数量增加1,来表明有一个资源被释放。如果有好几个任务被阻塞了,在等待资源,那么其中一个将会随后被取消阻塞,并被允许执行它的操作。

等待好几组在队列中的任务

dispatch goup是一种将线程阻塞直到一个或者多个任务结束执行的手段。你可以将这个操作用于:需要等待所有特定的任务都执行完成之后,才能继续进行任务的情况。例如,在分配了几个任务来计算一些数据之后,你可能需要使用一个组来等待那些任务,然后在它们全都执行完成之后,再使用计算的结果来进行下一步操作。另外一种使用dispatch group的情况就是,作为thread join的替代。不同于新开几个子线程再将他们并在一组,你可以将相关的任务添加到一个dispatch group并且等待整个组。
下表列出了建立一个组的基本步骤,还有如何将任务分配给它,并且等待结果。不同于使用dispatch_async函数来分配任务到队列中,你应该使用dispatch_group_async函数。这个函数将任务与组关联起来,并且将它放入队列中。还要等待一组任务完成,你需要使用dispatch_group_wait函数,向它传入响应的组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

// 将任务添加到组中
dispatch_group_async(group, queue, ^{
// 一些异步的操作
});

// 当任务正在运行的时候做一些其他的操作

// 当你不能继续执行操作
// 等待group那个group,并阻塞当前线程
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

// 当不再需要的时候,释放这个group
dispatch_release(group);

dispatch queue安全以及线程安全

这能在dispatch queue的环境下谈论线程安全有点奇怪,但是线程安全仍然是一个相关的话题。任何时候,你在程序中实现并发,都有几件事是你应该知道的:

  • dispatch queue他们本身是线程安全的。换句话说,你可以在任意线程提交任务到dispatch queue而不用实现使用线程锁或者对队列进行同步访问。
  • 不要向dispatch_sync传入与当前代码所在的相同的队列(如果当前代码是在某个队列中的block或者其他执行的话)。这样做会让队列发生死锁。如果你需要分配任务到当前队列,应该使用dispatch_async函数来异步地提交。
  • 避免对你提交到队列的任务使用lock。虽然在你的任务中使用lock是安全的,当你能获得lock。但是你正在冒险:当lock不可用的时候,一系列的队列将被阻塞。同样地,对于并发队列,等待一个lock可能会阻止其他任务的执行。如果你需要在你的代码中使用同步,那么你应该使用serial dispatch queue而不是lock。
  • 虽然你可以获取运行任务的底层线程的一些信息,但是最好还是避免这样做。关于更多的dispatch queue以及thread的一致性,戳这里

想知道更多的提示,关于如何将现有的线程代码转换为使用dispatch queue,戳这

(end,未校正)



以上