Threads(五)GCD - Dispatch Queues

Grand Central Dispatch(GCD) 调度队列(dispatch queues)是执行任务的强大工具,它使你可以相对于调用者异步或同步地执行任意代码块。适用于 ios 4.0+

与相应的线程代码相比,dispatch queues的优点是,使用起来更简单,能更有效地执行任务。

Dispatch Queues

  • Dispatch queue 是在应用程序中异步并发执行任务的一种简便方法。

  • task(任务) 只是应用程序需要执行的一些工作,如:可以定义一个任务来执行一些计算、创建或修改数据结构、处理从文件中读取的一些数据,或者执行任意数量的操作。通过将相应的代码放置在函数或block对象中并将其添加到dispatch queue来定义任务。

  • dispatch queue是一种类似对象的结构,用于管理提交给它的任务。所有dispatch queue都是first-in, first-out(先进先出)数据结构。因此,添加到队列中的任务总是按照添加时相同的顺序启动。GCD会自动为你提供一些dispatch queues,但也可以为特定目的创建其他dispatch queues

Dispatch Queues 的类型

串行队列

Serial queues(串行队列,又称 private dispatch queues) 按添加到队列的顺序一次执行一个任务。当前执行的任务运行在 dispatch queue 管理的不同线程上(可能因任务而异)。

Serial queues 通常用于同步对特定资源的访问。可以根据需要创建任意多个Serial queues,并且每个队列都与所有其他队列并行运行。

并发队列

Concurrent queues(并发队列,也称为 global dispatch queue 的一种) 可以并发执行一个或多个任务,但是任务仍然按照将它们添加到队列中的顺序启动。当前执行的任务在 dispatch queue 管理的不同线程上运行。在任何给定点执行的任务的确切数量是可变的,这取决于系统条件。

iOS 5及更高版本中,您可以通过将 DISPATCH_QUEUE_CONCURRENT 指定为队列类型来自己创建 concurrent dispatch queues。另外,有四个预定义的全局并发队列供你的应用程序使用。

主调度队列

main dispatch queue是全局可用的serial queue,它在应用程序的主线程上执行任务。

main dispatch queue 与 应用程序的run loop(如果存在)一起工作,将队列任务的执行 与 附加到run loop的其他event sources的执行 交织在一起。

因为main dispatch queue在应用程序的主线程上运行,所以它通常用作应用程序的关键同步点。尽管你无需创建main dispatch queue,但仍需要确保您的应用程序适当地消耗它。

dispatch queues的优点

在将并发性添加到应用程序时,dispatch queues比线程有几个优势。

1、最直接的好处是work-queue programming model(工作队列编程模型)的简单性。

使用线程,你必须为要执行的工作以及线程本身的创建和管理编写代码。Dispatch queues使你可以专注于实际要执行的工作,而无需担心线程的创建和管理。相反,系统将为你处理所有线程的创建和管理。优点是系统能够比任何单个应用程序更有效地管理线程。系统可以根据 可用资源 和 当前系统状况 动态缩放线程数。此外,系统通常能够比你自己创建线程更快地开始运行任务。

2、dispatch queues编写代码通常比为线程编写代码容易

虽然你可能认为为dispatch queues改写代码很困难,但是为dispatch queues编写代码通常比为线程编写代码容易。编写代码的关键是 设计独立 且 能够异步运行的任务(实际上,对于线程和dispatch queues都是如此)。

3、dispatch queues的优势在于可预测性

如果有两个任务访问相同的共享资源,但在不同的线程上运行,则每个线程都可以首先修改资源,并且需要使用锁来确保这两个任务不会同时修改该资源。使用dispatch queues,你可以将两个任务都添加到serial dispatch queue中,以确保在任何给定时间只有一个任务修改了资源。这种基于queue-based synchronization(队列的同步)比locks更有效,因为在有争用还是无争用的情况下,locks总是需要昂贵的kernel trap(内核陷阱),而dispatch queues主要在应用程序的进程空间中工作,并且只有在绝对必要时才调用内核。

虽然你指出在 serial queue 中运行的两个任务不能同时运行是正确的,但你必须记住,如果两个线程同时获得一个lock,则线程提供的任何并发性都会丢失或显着减少。更重要的是,线程模型需要创建两个线程,这两个线程同时占用内核和用户空间内存。Dispatch queues 不会为它们的线程支付相同的内存损失,它们使用的线程会一直保持忙碌,不会阻塞。

4、dispatch queues 的一些要点:

  • dispatch queues 相对于其他 dispatch queues 并行执行任务。任务的序列化仅限于单个 dispatch queues 中的任务。

  • 系统决定任何一次执行的任务总数。因此,在100个不同的queues中具有100个任务的应用程序可能不会并发执行所有这些任务(除非它具有100个或更多个有效内核)。

  • 在选择启动哪个新任务时,系统会考虑queue priority levels(队列优先级)。可以设置serial queue优先级。

  • queue 中的任务在添加到 queue 时必须准备就绪才能执行。(如果以前使用过Cocoa operation objects,请注意,此行为与model operations使用的行为有所不同。)

  • private dispatch queues(即,串行队列) 是引用计数的对象(reference-counted objects)。请注意,除了将 queue 保留在自己的代码中之外,还可以将dispatch sources附加到 queue 并增加其 retain count。因此,你必须确保取消所有dispatch sources,并且所有retain调用 都与适当的release调用平衡。

队列相关技术

除了dispatch queuesGrand Central Dispatch还提供了几种使用 queues 的技术来帮助您管理代码。

Dispatch groups

dispatch groups(调度组) 是一种监视一组 block 对象是否完成的方法,你可以根据需要同步或异步监视这些blockGroups为依赖于其他任务完成的代码 提供了一种有用的同步机制。

Dispatch semaphores

Dispatch semaphores(调度信号量)与传统信号量相似,但通常效率更高。只有在由于semaphore不可用 而需要阻塞调用线程时,才将Dispatch semaphores向下调用到内核。如果semaphore可用,则不进行内核调用。

Dispatch sources

Dispatch sources(调度源) 响应于特定类型的系统事件而生成通知。可以使用 dispatch sources 来监视事件,例如进程通知、信号和描述符事件。发生事件时,dispatch source将任务代码异步提交到指定的dispatch queue进行处理。

使用block执行任务

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

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

你可以使用类似于函数指针的语法来声明与代码内联的块(blocks inline)。blockfunction pointer之间的主要区别在于,block名称的前面带有^符号而不是*符号。像function pointer一样,你可以将参数传递给block并从中接收返回值。

1
2
3
4
5
6
7
8
int x = 123;
int y = 456;

void (^aBlock)(int) = ^(int z) {
printf("%d %d %d\n", x, y, z);
};

aBlock(789); // prints: 123 456 789

以下是设计blocks时应考虑的一些关键准则的总结:

  • 对于计划使用dispatch queue异步执行的blocks,从父函数或方法中捕获标量变量并在block中使用它们是安全的。但是,你不应该试图捕获大型结构 或 其他由调用上下文分配和删除的基于指针的变量。在执行block时,该指针引用的内存可能会消失。当然,自己分配内存(或对象)并显式地将该内存的所有权交给block是安全的。

  • dispatch queues 复制添加到它们的blocks,并在完成执行时释放blocks。换句话说,在将blocks添加到queue之前,无需显式复制块。

  • 尽管在执行小任务时,queues比原始线程更有效,但是创建blocks并在queues上执行它们仍然有开销。如果一个blocks做的工作太少,则内联执行它可能比将其分派到队列中更便宜。判断某个blocks是否工作量太少的方法是使用性能工具收集每个路径的指标并进行比较。

  • 不要缓存与底层线程相关的数据,并期望可以从不同的block访问这些数据。如果同一队列中的任务需要共享数据,请改用dispatch queue的上下文指针存储数据。

  • 如果你的block创建了多个Objective-C对象,你可能希望将block部分代码的代码包含在@autorelease block中,以处理这些对象的内存管理。 尽管GCD dispatch queues具有自己的autorelease pools,但是它们不保证这些pools何时耗尽。如果您的应用程序受内存限制,则创建自己的autorelease pool可以让你在更定期地为自动释放对象释放内存。

创建和管理 Dispatch Queues

在将任务添加到queue之前,必须确定要使用的队列类型以及打算如何使用它。Dispatch queues可以串行或并行执行任务。此外,如果你对队列有特定的用途,则可以相应地配置queue属性。

获取 Global Concurrent Dispatch Queues

当你有多个可以并行运行的任务时,concurrent dispatch queue很有用。concurrent queue 仍然是一个 queue,因为它按照first-in, first-out(先进先出)的顺序将任务从队列中取出。但是,concurrent queue可能会在其他先前任务完成之前使其他任务出队。concurrent queue在任何给定时刻执行的实际任务数是可变的,可以随着应用程序条件的变化而动态变化。许多因素会影响concurrent queue执行的任务数量,包括可用内核数、其他进程正在完成的工作量以及其他serial dispatch queues中任务的数量和优先级。

系统为每个应用程序提供4个concurrent dispatch queues。这些queues对于应用程序来说是全局的,仅按优先级进行区分。因为它们是全局的,所以无需显式创建它们,只需要使用 dispatch_get_global_queue 函数请求其中一个队列,如以下示例所示:

1
2
3
4
5
6
7
8
/* 通过传递常量获取 queue
DISPATCH_QUEUE_PRIORITY_HIGH 高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW 低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND // 后台队列
注意:第二个参数保留给以后使用,目前始终为该参数传递0。
*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

尽管dispatch queuesreference-counted objects(引用计数的对象),但是你无需retainrelease global concurrent queues,因为它们在应用程序中是全局的,所以忽略对这些queues调用retainrelease。因此,不需要存储对这些queues的引用,只要需要对其中一个队列的引用,就可以调用dispatch_get_global_queue 函数。

创建 Serial Dispatch Queues

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

与创建的concurrent queues不同,你必须显式创建和管理要使用的任何serial queue。可以为应用程序创建任意数量的serial queues,但是应该避免仅仅为了同时执行尽可能多的任务而创建大量的serial queues。如果要同时执行大量任务,请将它们提交到一个global concurrent queues中。创建serial queues时,请尝试确定每个queue的用途,例如保护资源或同步应用程序的某些关键行为。

创建自定义串行队列

1
2
3
4
5
/*
参数一:表示 队列名称,用于调试器和性能工具,以跟踪任务的执行方式
参数二:为将来使用而保留的,应该为 NULL
*/
dispatch_queue_t queue = dispatch_queue_create("com.example.MyQueue", NULL);

在运行时获取 Common Queues

除了你创建的任何自定义队列之外,系统还会自动创建一个serial queue并将其绑定到应用程序的主线程。

GCD提供了一些函数,使你可以从应用程序中访问几个常见的dispatch queues

  • 使用 dispatch_get_current_queue 函数进行调试或测试当前队列的身份。从block对象内部调用此函数将返回已提交该block的队列,并且现在可能正在该队列上运行。从block外部调用此函数将为您的应用程序返回default concurrent queue

  • 使用 dispatch_get_main_queue 函数获取与应用程序的主线程关联的serial dispatch queue。此队列是为Cocoa应用程序以及调用了dispatch_main函数或在主线程上配置run loop的应用程序 自动创建的。

  • 使用 dispatch_get_global_queue 函数获取任何共享的concurrent queues

Dispatch Queues的内存管理

Dispatch queues和其它dispatch objectsreference-counted data types(引用计数的数据类型)。创建serial dispatch queue时,它的初始引用计数为1。可以根据需要使用dispatch_retaindispatch_release函数来递增和递减该引用计数。当 queue 的引用计数达到零时,系统将异步释放该queue

retainrelease dispatch objects很重要,以确保它们在使用时仍保留在内存中。与内存管理的Cocoa对象一样,一般规则是,如果你打算使用传递给代码的queue,则应在使用该queue之前保留该queue,并在不再需要它时release它。这种基本模式可确保queue在你使用期间一直保留在内存中。

注意:你不需要保留或释放任何global dispatch queues,包括concurrent dispatch queuesmain dispatch queueretainrelease``queues的任何尝试都将被忽略。

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

所有dispatch objects(包括dispatch queues)都允许你将自定义上下文数据与该对象相关联。要在给定对象上设置和获取此数据,请使用dispatch_set_contextdispatch_get_context函数。系统不会以任何方式使用你的自定义数据,你可以在适当的时间分配和取消分配数据。

对于queues,可以使用上下文数据存储指向Objective-C对象或其他数据结构的指针,该指针或数据结构有助于识别queues或其在代码中的预期用途。可以使用queuefinalizer函数在释放上下文数据之前将其从queue中释放(或取消关联)。

为Queue提供清理功能

创建serial dispatch queue后,可以附加finalizer函数,以在释放queue时执行任何自定义清理。Dispatch queues是引用计数对象,可以使用dispatch_set_finalizer_f函数指定当queue的引用计数达到零时要执行的函数。可以使用此函数来清理与queue关联的上下文数据,并且仅当上下文指针不为NULL时才调用此函数。

向Queue添加任务

要执行任务,必须将其分派到适当的dispatch queue。可以同步或异步分配任务,也可以单个或成组分配任务。一旦进入队列,该队列就会根据其约束条件和队列中已经存在的任务,负责尽快执行任务。

向 Queue 添加单个任务

queue添加任务有两种方法:asynchronously(异步)或synchronously(同步)。

如果可能,使用dispatch_asyncdispatch_async_f函数进行异步执行优于同步替代方案。当你向queue添加block对象或函数时,无法知道该代码何时执行。因此,异步添加block或函数使你可以安排代码的执行,并继续从调用线程执行其他工作。如果你正在从应用程序的主线程调度任务,这一点尤其重要,这可能是为了响应某些用户事件。

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

向并发队列添加任务

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_queue_t queueC = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_sync(queueC, ^{
NSLog(@"同步函数-并发队列-%@", [NSThread currentThread]);
});

dispatch_async(queueC, ^{
NSLog(@"异步函数-并发队列-%@", [NSThread currentThread]);
});

// 打印:
同步函数-并发队列-<NSThread: 0x6000037c01c0>{number = 1, name = main}
异步函数-并发队列-<NSThread: 0x600003798780>{number = 8, name = (null)}

向串行队列添加任务

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

dispatch_sync(queueS, ^{
NSLog(@"同步函数-串行队列-%@", [NSThread currentThread]);
});

dispatch_async(queueS, ^{
NSLog(@"异步函数-串行队列-%@", [NSThread currentThread]);
});

// 打印:
同步函数-串行队列-<NSThread: 0x6000036641c0>{number = 1, name = main}
异步函数-串行队列-<NSThread: 0x600003624380>{number = 8, name = (null)}

向主队列添加任务

  • 只能以异步的方式向主队列添加任务
1
2
3
4
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"异步函数-主队列-%@", [NSThread currentThread]);
});
// 打印:异步函数-主队列-<NSThread: 0x6000003583c0>{number = 1, name = main}

决不能从计划传递给函数的同一 queue 中执行的任务调用dispatch_syncdispatch_sync_f函数。这对于serial queues特别重要,serial queues必然会发生死锁,但对于concurrent queues也应该避免。

造成死锁的情况如下:

1
2
3
4
// 在主线程执行如下函数会造成死锁崩溃
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"同步函数-主队列-%@", [NSThread currentThread]);
});
1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t queueS = dispatch_queue_create("com.test.myQueue1", NULL);

dispatch_async(queueS, ^{
NSLog(@"%@", [NSThread currentThread]);
// 在同一queue 中 同步添加任务,会造成死锁崩溃
// dispatch_queue_t queueS = dispatch_get_current_queue();
dispatch_sync(queueS, ^{
NSLog(@"%@", [NSThread currentThread]);
});
NSLog(@"xxcv");
});

完成任务时执行 Completion Block

从本质上讲,分派到queue的任务独立于创建它们的代码运行。但是,当任务完成时,应用程序可能仍然希望得到通知,以便它能够合并结果。在传统的异步编程中,你可以使用回调机制来执行此操作,但是在dispatch queues中,可以使用completion block

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

1
2
3
4
5
6
7
8
9
10
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSLog(@"1---%@", [NSThread currentThread]);

// 任务完成,调用完成块
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"2---%@", [NSThread currentThread]);
});

});

同时执行循环迭代

concurrent dispatch queues 可能提高性能的一个地方是,你有一个执行固定次数迭代的循环。如下for循环,该循环每次迭代中都完成了一些工作:

1
2
3
for (int i = 0; i < 100; i++) {
NSLog(@"%d", i);
}

如果在每次迭过程中执行的工作与在所有其他迭代过程中执行的工作不同,并且每个连续循环完成的顺序不重要,则可以用调用dispatch_applydispatch_apply_f函数来替换循环。这些函数在每次循环迭代一次将指定的block或函数提交给queue。 因此,当分派到concurrent queue时,可以同时执行多个循环迭代。

在调用dispatch_applydispatch_apply_f时,可以指定serial queueconcurrent queue。传递concurrent queue使你可以同时执行多个循环迭代,这是使用这些函数的最常用方法。尽管使用serial queue是允许的,并且对代码做了正确的事情,但是使用这样的queue与保留循环相比没有真正的性能优势。

与常规的for循环一样,dispatch_applydispatch_apply_f函数只有在完成所有循环迭代之后才返回。因此,当从已经在队列上下文中执行的代码中调用它们时,应该小心。如果作为参数传递给函数的队列是一个serial queue,并且与执行当前代码的队列相同,则调用这些函数将使queue死锁。

因为它们有效地阻塞了当前线程,所以从主线程调用这些函数时也应小心,因为它们可能阻止事件处理循环及时响应事件。如果循环代码需要大量的处理时间,则可能需要从其他线程调用这些函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dispatch_group_t downloadGroup = dispatch_group_create();

dispatch_apply(100, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t i) {

dispatch_group_enter(downloadGroup);

// 开始执行循环体任务
NSLog(@"%zu---%@", i, [NSThread currentThread]);

dispatch_group_leave(downloadGroup);
});

dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
NSLog(@"循环执行完成---%@", [NSThread currentThread]);
});

应该确保你的任务代码在每次迭代中完成了合理的工作量。与分派给queue的任何block或函数一样,调度代码执行也会有开销。如果循环的每次迭代仅执行少量工作,则调度代码的开销 可能会超过 将代码调度到queue可能带来的性能优势。如果在测试期间发现这是正确的,则可以使用striding(跨步)来增加每次循环迭代期间执行的工作量。 使用striding,可以将原始循环的多个迭代组合到一个block中,并按比例减少迭代计数。例如,如果最初执行100次迭代,但决定使用4的步幅,则现在从每个块执行4次循环迭代,你的迭代计数为25。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dispatch_group_t downloadGroup = dispatch_group_create();

int count = 100, stride = 10;

dispatch_apply(count / stride, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t idx) {

dispatch_group_enter(downloadGroup);

// 开始执行循环体任务
size_t j = idx * stride;
size_t j_stop = j + stride;

do {
NSLog(@"%zu---%@", j++, [NSThread currentThread]);
} while (j < j_stop);

dispatch_group_leave(downloadGroup);
});

dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
NSLog(@"循环执行完成---%@", [NSThread currentThread]);
});

在主线程上执行任务

Grand Central Dispatch提供了一个特殊的dispatch queue,用于在应用程序的主线程上执行任务。所有应用程序均会自动提供此queue,并由在其主线程上设置run loop的任何应用程序自动耗尽。如果不创建Cocoa应用程序,并且不想显式设置run loop,则必须调用dispatch_main函数显式地耗尽main dispatch queue。你仍然可以将任务添加到队列中,但是如果您不调用此函数,则这些任务将永远不会执行。

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

在任务中使用Objective-C对象

GCDCocoa内存管理技术提供了内置支持,因此可以在提交给dispatch queuesblocks中自由使用Objective-C对象。每个dispatch queue都维护自己的autorelease pool,以确保自动释放的对象在某个时刻被释放。queues并不保证它们何时真正释放这些对象。

如果您的应用程序受内存限制,而block创建了多个自动释放的对象,则创建自己的autorelease pool是确保及时释放对象的唯一方法。如果block创建了数百个对象,则可能要创建多个autorelease pool或定期清空你的pool

挂起和恢复队列

可以通过挂起一个queue来临时阻止它执行block对象。可以使用dispatch_suspend函数挂起dispatch queue,然后使用dispatch_resume函数将其恢复。调用dispatch_suspend会使队列的挂起引用计数增加,而调用dispatch_resume会使引用计数减少。当引用计数大于零时,队列保持挂起状态。因此,你必须平衡所有suspend的调用与匹配的resume调用,以恢复处理block

Suspend(暂停)和resume(恢复)调用是异步的,仅在执行block之间生效。挂起一个队列不会导致已经执行的block停止。

使用 Dispatch Semaphores 调节有限资源的使用

如果要提交给dispatch queues的任务访问某个有限资源,则可能要使用dispatch semaphore(调度信号量)来调节同时访问该资源的任务数。dispatch semaphore的工作方式类似于常规信号量,但有一个例外。当资源可用时,获取dispatch semaphore 的时间比获取传统system semaphore的时间要短。这是因为在这种特殊情况,Grand Central Dispatch不会调用内核,它唯一调用内核的时间是当资源不可用时,系统需要停止线程,直到信号量发出信号。

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

  1. 创建semaphore时(使用dispatch_semaphore_create函数),可以指定一个正整数,表示可用资源的数量。

  2. 在每个任务中,调用dispatch_semaphore_wait等待semaphore

  3. 等待调用返回时,获取资源并进行工作。

  4. 处理完资源后,请释放该资源,并通过调用dispatch_semaphore_signal函数来发出semaphore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 可用资源浴室为2间
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);

// 下面安排10个人去洗澡
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
// 如果资源数量为0,会阻塞当前线程。根据等待时间进行等待
// dispatch_time_t timeOut = dispatch_time(DISPATCH_TIME_NOW, 3);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"第%d个人开始洗澡,时间为: %@", i, [NSDate date]);
sleep(3);
dispatch_semaphore_signal(semaphore);
});
}

创建信号量时,请指定可用资源的数量。该值成为semaphore(信号量)的初始计数变量。每次等待semaphore(信号量)时,dispatch_semaphore_wait函数都会将对变量的计数减1。如果结果值为负,则该函数会通知内核阻止您的线程。 另一方面,dispatch_semaphore_signal函数将count变量增加1,以指示资源已释放。如果有任务被阻塞并等待资源,则其中一个任务随后将被解除阻塞并允许执行其工作。

等待排队的任务组

Dispatch groups(调度组)是一种阻塞线程,直到一个或多个任务完成执行的一种方法。可以在所有指定任务完成之前无法取得进展的地方使用它。

使用方式一:在分派了多个任务以计算一些数据之后,使用一个组来等待这些任务,然后在完成后处理结果;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{

sleep(5);
NSLog(@"1---%@", [NSThread currentThread]);
dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(3);
NSLog(@"2---%@", [NSThread currentThread]);
dispatch_group_leave(group);
});

dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务完成");
});

使用方式二:使用dispatch groups的另一种方法是替代线程连接。可以将相应的任务添加到dispatch group并等待整个组,而不是启动几个子线程,然后再与每个子线程联接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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, ^{
sleep(3);
NSLog(@"1---%@", [NSThread currentThread]);
});

dispatch_group_async(group, queue, ^{
sleep(5);
NSLog(@"2---%@", [NSThread currentThread]);
});

// 同步任务完成的任务
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"完成---%@", [NSThread currentThread]);
});

// 当组内的任务未完成时,会阻塞当前线程。
// dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

Dispatch Queues和线程安全

dispatch queues 的上下文中谈论线程安全性似乎很奇怪,但是线程安全性仍然是一个相关主题。每当你在应用程序中实现并发时,你都应该了解以下几点:

  • Dispatch queues本身是线程安全的。换句话说,可以从系统上的任何线程将任务提交到dispatch queue,而无需先对该queue进行 锁定 或 同步对该queue访问。

  • 不要从一个正在执行的任务中调用dispatch_sync函数,这个任务和你传递给你的函数调用的是同一个队列。这样做将导致队列死锁。如果需要dispatch到当前queue,请使用dispatch_async函数异步进行。

  • 避免从提交到dispatch queue的任务中获取locks。虽然从任务中使用lock是安全的,但是当你获取lock时,如果该lock不可用,则可能会完全阻塞serial queue。同样,对于concurrent queues,等待lock可能会阻止其他任务执行。如果需要同步部分代码,请使用serial dispatch queue而不是lock

  • 尽管您可以获得有关运行任务的基础线程的信息,但最好避免这样做。

学习博客

Dispatch Queues

iOS 详细介绍 GCD

文章作者: Czm
文章链接: http://yoursite.com/2020/10/23/Threads-%E4%BA%94-GCD-Dispatch-Queues/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm