iOS多线程之并发编程-2

Dispatch Queues

dispatch queues是执行任务的强大工具,允许你同步或异步地执行任意代码block。原先使用单独线程执行的所有任务都可以替换为使用dispatch queues。而dispatch queues最大的优点在于使用简单,而且更加高效。

dispatch queues任务的概念就是应用需要执行的一些工作,如计算、创建或修改数据结构、处理数据等等。我们使用函数或block对象来定义任务,并添加到dispatch queue。

dispatch queue是类似于对象的结构体,管理你提交给它的任务,而且都是先进先出的数据结构。因此queue中的任务总是以添加的顺序开始执行。Grand Central Disaptch提供了几种dispatch queues,不过你也自己创建。

类型 描述

串行 也称为private dispatch queue,每次只执行一个任务,按任务添加顺序执行。当前正在执行的任务在独立的线程中运行(不同任务的线程可能不同),dispatch queue管理了这些线程。通常串行queue主要用于对特定资源的同步访问。

你可以创建任意数量的串行queues,虽然每个queue本身每次只能执行一个任务,但是各个queue之间是并发执行的。

并发 也称为global dispatch queue,可以并发执行一个或多个任务,但是任务仍然是以添加到queue的顺序启动。每个任务运行于独立的线程中,dispatch queue管理所有线程。同时运行的任务数量随时都会变化,而且依赖于系统条件。

你不能创建并发dispatch queues。相反应用只能使用三个已经定义好的全局并发queues。

Main dispatch queue 全局可用的串行queue,在应用主线程中执行任务。这个queue与应用的 run loop 交叉执行。由于它运行在应用的主线程,main queue通常用于应用的关键同步点。

虽然你不需要创建main dispatch queue,但你必须确保应用适当地回收

应用使用dispatch queue,相比线程有很多优点,最直接的优点是简单,不用编写线程创建和管理的代码,让你集中精力编写实际工作的代码。另外系统管理线程更加高效,并且可以动态调控所有线程。

dispatch queue比线程具有更强的可预测性,例如两个线程访问共享资源,你可能无法控制哪个线程先后访问;但是把两个任务添加到串行queue,则可以确保两个任务对共享资源的访问顺序。同时基于queue的同步也比基于锁的线程同步机制更加高效。

应用有效地使用dispatch queue,要求尽可能地设计自包含、可以异步执行的任务。

dispatch queues的几个关键点:

dispatch queues相对其它dispatch queues并发地执行任务,串行化任务只能在同一个dispatch queue中实现。

系统决定了同时能够执行的任务数量,应用在100个不同的queues中启动100个任务,并不表示100个任务全部都在并发地执行(除非系统拥有100或更多个核)

系统在选择执行哪个任务时,会考虑queue的优先级。

queue中的任务必须在任何时候都准备好运行,注意这点和Operation对象不同。

private dispatch queue是引用计数的对象。你的代码中需要retain这些queue,另外dispatch source也可能添加到一个queue,从而增加retain的计数。因此你必须确保所有dispatch source都被取消,而且适当地调用release。

Queue相关的技术

除了dispatch queue,Grand Central Disaptch还提供几个相关的技术,使用queue来帮助你管理代码。

技术 描述

Dispatch group 用于监控一组block对象完成(你可以同步或异步地监控block)。Group提供了一个非常有用的同步机制,你的代码可以等待其它任务的完成

Dispatch semaphore 类似于传统的semaphore(信号量),但是更加高效。只有当调用线程由于信号量不可用,需要阻塞时,Dispatch semaphore才会去调用内核。如果信号量可用,就不会与内核进行交互。使用信号量可以实现对有限资源的访问控制

Dispatch source Dispatch source在特定类型的系统事件发生时,会产生通知。你可以使用dispatch source来监控各种事件,如:进程通知、信号、描述符事件、等等。当事件发生时,dispatch source异步地提交你的任务到指定的dispatch queue,来进行处理。使用Block实现任务

Block可以非常容易地定义“自包含”的工作单元,尽管看上去非常类似于函数指针,block实际上由底层数据结构来表示,由编译器负责创建和管理。编译器对你的代码(和所有相关的数据)进行打包,封装为可以存在于堆中的格式,并在你的应用各个地方传递。

Block最关键的优点能够使用own lexical scope之外的变量,在函数或方法内部定义一个block时,block可以直接读取父scope中的变量。block访问的变量全部被拷贝到block在堆中的数据结构,这样block就能在稍后自由地访问这些变量。当block被添加到dispatch queue中时,这些变量通常是只读格式的。不过同步执行的Block对象,可以使用那些定义为__block的变量,对这些变量的修改会影响到调用scope。

Block的简单用法:

int x = 123;

int y = 456;

// Block declaration and assignment

void (^aBlock)(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中安全地捕获和使用父函数或方法中的scalar变量。但是Block不应该去捕获大型结构体或其它基于指针的变量,这些变量由Block的调用上下文分配和删除。在你的Block被执行时,这些指针引用的内存可能已经不存在。当然,你自己显式地分配内存(或对象),然后让Block拥有这些内存的所有权,是安全可行的。

Dispatch queue对添加的Block会进行复制,在完成执行后自动释放。换句话说,你不需要在添加Block到Queue时显式地复制

尽管Queue执行小任务比原始线程更加高效,仍然存在创建Block和在Queue中执行的开销。如果Block做的事情太少,可能直接执行比dispatch到queue更加有效。使用性能工具来确认Block的工作是否太少

绝对不要针对底层线程缓存数据,然后期望在不同Block中能够访问这些数据。如果相同queue中的任务需要共享数据,应该使用dispatch queue的context指针来存储这些数据。

如果Block创建了大量Objective-C对象,考虑创建自己的autorelease pool,来处理这些对象的内存管理。虽然dispatch queue也有自己的autorelease pool,但不保证在什么时候会回收这些pool。

创建和管理Dispatch Queue

获得全局并发Dispatch Queue

并发dispatch queue可以同时并行地执行多个任务,不过并发queue仍然按先进先出的顺序来启动任务,并发queue会在之前任务完成之前就出列下一个任务并启动执行。并发queue同时执行的任务数量会根据应用和系统动态变化,各种因素包括:可用核数量、其它进程正在执行的工作数量、其它串行dispatch queue中优先任务的数量等。

系统给每个应用提供三个并发dispatch queue,所有应用全局共享,三个queue的区别是优先级。你不需要显式地创建这些queue,使用 dispatch_get_global_queue 函数来获取这三个queue:

dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

除了默认优先级的并发queue,你还可以获得高和低优先级的两个,分别使用 DISPATCH_QUEUE_PRIORITY_HIGH 和 DISPATCH_QUEUE_PRIORITY_LOW 常量来调用上面函数。

虽然dispatch queue是引用计数的对象,但你不需要retain和release全局并发queue。因为这些queue对应用是全局的,retain和release调用会被忽略。

你也不需要存储这三个queue的引用,每次都直接调用 dispatch_get_global_queue 获得queue就行了。

创建串行Dispatch Queue

应用的任务需要按特定顺序执行时,就需要使用串行Dispatch Queue,串行queue每次只能执行一个任务。你可以使用串行queue来替代锁,保护共享资源或可变的数据结构。和锁不一样的是,串行queue确保任务按可预测的顺序执行。而且只要你异步地提交任务到串行queue,就永远不会产生死锁。

你必须显式地创建和管理所有你使用的串行queue,应用可以创建任意数量的串行queue,但不要为了同时执行更多任务而创建更多的串行queue。如果你需要并发地执行大量任务,应该把任务提交到全局并发Queue。

创建串行queue时,你需要明确自己的目的,如保护共享资源,或同步应用的某些关键行为。

dispatch_queue_create 函数创建串行queue,两个参数分别是queue名和一组queue属性。调试器和性能工具会显示queue的名字,便于你跟踪任务的执行。

dispatch_queue_t queue;

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

运行时获得公共Queue

Grand Central Disaptch提供函数,让应用访问几个公共dispatch queue:

使用 dispatch_get_current_queue 函数作为调试用途,或者测试当前queue的标识。在block对象中调用这个函数会返回block提交到的queue(这个时候queue应该正在执行中)。在block对象之外调用这个函数会返回应用的默认并发queue。

使用 dispatch_get_main_queue 函数获得应用主线程关联的串行dispatch queue。Cocoa 应用、调用了 dispatch_main 函数或配置了run loop(CFRunLoopRef 类型 或一个 NSRunLoop 对象)的应用,会自动创建这个queue。

使用 dispatch_get_global_queue 来获得共享的并发queue

Dispatch Queue的内存管理

Dispatch Queue和其它dispatch对象都是引用计数的数据类型。当你创建一个串行dispatch queue时,初始引用计数为1,你可以使用 dispatch_retain 和 dispatch_release 函数来增加和减少引用计数。当引用计数到达0时,系统会异步地销毁这个queue。

对dispatch对象(如queue)retain和release是很重要的,确保它们被使用时能够保留在内存中。和内存托管的Cocoa对象一样,通用的规则是如果你使用一个传递给你代码中的queue,你应该在使用前retain,使用完之后release。

你不需要retain或release全局dispatch queue,包括全局并发 dispatch queue和main dispatch queue。

即使你实现的是自动垃圾收集的应用,也需要retain和release你的dispatch queue和其它dispatch对象。Grand Central Disaptch不支持垃圾收集模型来回收内存。

在Queue中存储自定义上下文信息

所有dispatch对象(包括dispatch queue)都允许你关联custom context data。使用 dispatch_set_context 和 dispatch_get_context 函数来设置和获取对象的上下文数据。系统不会使用你的上下文数据,所以需要你自己在适当的时候分配和销毁这些数据。

对于Queue,你可以使用上下文数据来存储一个指针,指向Objective-C对象或其它数据结构,协助标识这个queue或代码的其它用途。你可以使用queue的finalizer函数来销毁(或解除关联)上下文数据。

为Queue提供一个清理函数

在创建串行dispatch queue之后,可以附加一个finalizer函数,在queue被销毁之前执行自定义的清理操作。使用 dispatch_set_finalizer_f 函数为queue指定一个清理函数,当queue的引用计数到达0时,就会执行该清理函数。你可以使用清理函数来解除queue关联的上下文数据,而且只有上下文指针不为NULL时才会调用这个清理函数。

下面例子演示了自定义finalizer函数的使用,你需要自己提供 myInitializeDataContextFunction 和 myCleanUpDataContextFunction 函数,用于初始化和清理上下文数据。

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

if (serialQueue)

{

dispatch_set_context(serialQueue, data);

dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);

}

return serialQueue;

}

添加任务到Queue

要执行一个任务,你需要将它dispatch到一个适当的dispatch queue,你可以同步或异步地dispatch一个任务,也可以单个或按组来dispatch。一旦进入到queue,queue会负责尽快地执行你的任务。

添加单个任务到Queue

你可以异步或同步地添加一个任务到Queue,尽可能地使用 dispatch_async 或 dispatch_async_f 函数异步地dispatch任务。因为添加任务到Queue中时,无法确定这些代码什么时候能够执行。因此异步地添加block或函数,可以让你立即调度这些代码的执行,然后调用线程可以继续去做其它事情。

特别是应用主线程一定要异步地dispatch任务,这样才能及时地响应用户事件。

少数时候你可能希望同步地dispatch任务,以避免竞争条件或其它同步错误。使用 dispatch_sync 和 dispatch_sync_f 函数同步地添加任务到Queue,这两个函数会阻塞,直到相应任务完成执行。

绝对不要在任务中调用 dispatch_sync 或 dispatch_sync_f 函数,并同步dispatch新任务到当前正在执行的queue。对于串行queue这一点特别重要,因为这样做肯定会导致死锁;而并发queue也应该避免这样做。

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

dispatch到queue中的任务,通常与创建任务的代码独立运行。在任务完成时,应用可能希望得到通知并使用任务完成的结果数据。在传统的异步编程模型中,你可能会使用回调机制,不过dispatch queue允许你使用Completion Block。

Completion Block是你dispatch到queue的另一段代码,在原始任务完成时自动执行。调用代码在启动任务时通过参数提供Completion Block。任务代码只需要在完成工作时提交指定的Block或函数到指定的queue。

下面代码使用block实现了平均数,最后两个参数允许调用方指定一个queue和报告结果的block。在平均数函数完成计算后,会传递结果到指定的block,并dispatch到指定的queue。为了防止queue被过早地释放,必须首先retain这个queue,然后在dispatch这个Completion Block之后,再release这个queue。

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

});

}

并发地执行Loop Iteration

如果你使用循环执行固定次数的迭代,并发dispatch queue可能会提高性能。例如下面for循环:

for (i = 0; i < count; i++) {

printf("%u\n",i);

}

如果每次迭代执行的任务与其它迭代独立无关,而且循环迭代执行顺序也无关紧要的话,你可以调用 dispatch_apply 或 dispatch_apply_f 函数来替换循环。这两个函数为每次循环迭代将指定的block或函数提交到queue。当dispatch到并发queue时,就有可能同时执行多个循环迭代。

调用 dispatch_apply 或 dispatch_apply_f 时你可以指定串行或并发queue。并发queue允许同时执行多个循环迭代,而串行queue就没太大必要使用了。

和普通for循环一样,dispatch_apply 和 dispatch_apply_f 函数也是在所有迭代完成之后才会返回。因此在queue上下文执行的代码中再次调用这两个函数时,必须非常小心。如果你传递的参数是串行queue,而且正是执行当前代码的Queue,就会产生死锁。

另外这两个函数还会阻塞当前线程,因此在主线程中调用这两个函数同样必须小心,可能会阻止事件处理循环并无法响应用户事件。所以如果循环代码需要一定的时间执行,你可以考虑在另一个线程中调用这两个函数。

下面代码使用 dispatch_apply 替换了for循环,你传递的block必须包含一个参数,用来标识当前循环迭代。第一次迭代这个参数值为0,第二次时为1,最后一次值为count - 1。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(count, queue, ^(size_t i) {

printf("%u\n",i);

});

循环迭代执行的工作量需要仔细平衡,太多的话会降低响应性;太少则会影响整体性能,因为调度的开销大于实际执行代码。

在主线程中执行任务

Grand Central Disaptch提供一个特殊dispatch queue,可以在应用的主线程中执行任务。应用主线程设置了run loop(由CFRunLoopRef 类型或 NSRunLoop 对象管理),就会自动创建这个queue,并且自动drain。非Cocoa应用如果不显式地设置run loop,就必须显式地调用dispatch_main 函数来显式地drain这个dispatch queue。否则虽然你可以添加任务到queue,但任务永远不会被执行。

调用 dispatch_get_main_queue 函数获得应用主线程的dispatch queue。添加到这个queue的任务由主线程串行化执行,因此你可以在应用的某些地方使用这个queue作为同步点。

任务中使用Objective-C对象

Grand Central Disaptch支持Cocoa内存管理机制,因此可以在提交到queue的block中自由地使用Objective-C对象。每个dispatch queue维护自己的autorelease pool确保释放autorelease对象,但是queue不保证这些对象实际释放的时间。在自动垃圾收集的应用中,Grand Central Disaptch会在垃圾收集系统中注册自己创建的每个线程。

如果应用消耗大量内存,并且创建大量autorelease对象,你需要创建自己的autorelease pool,用来及时地释放不再使用的对象。

挂起和继续queue

我们可以暂停一个queue以阻止它执行block对象,使用 dispatch_suspend 函数挂起一个dispatch queue;使用 dispatch_resume 函数继续dispatch queue。调用 dispatch_suspend 会增加queue的引用计数,调用 dispatch_resume 则减少queue的引用计数。当引用计数大于0时,queue就保持挂起状态。因此你必须对应地调用suspend和resume函数。

挂起和继续是异步的,而且只在执行block之间生效。挂起一个queue不会导致正在执行的block停止。

使用Dispatch Semaphore控制有限资源的使用

如果提交到dispatch queue中的任务需要访问某些有限资源,可以使用dispatch semaphore来控制同时访问这个资源的任务数量。dispatch semaphore和普通的信号量类似,唯一的区别是当资源可用时,需要更少的时间来获得dispatch semaphore。

使用dispatch semaphore的过程如下:

使用 dispatch_semaphore_create 函数创建semaphore,指定正数值表示资源的可用数量。

在每个任务中,调用 dispatch_semaphore_wait 来等待Semaphore

当上面调用返回时,获得资源并开始工作

使用完资源后,调用 dispatch_semaphore_signal 函数释放和signal这个semaphore

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

等待queue中的一组任务

Dispatch group用来阻塞一个线程,直到一个或多个任务完成执行。有时候你必须等待任务完成的结果,然后才能继续后面的处理。dispatch group也可以替代线程join。

基本的流程是设置一个组,dispatch任务到queue,然后等待结果。你需要使用 dispatch_group_async 函数,会关联任务到相关的组和queue。使用 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 Queue和线程安全性

使用Dispatch Queue实现应用并发时,也需要注意线程安全性:

Dispatch queue本身是线程安全的。换句话说,你可以在应用的任意线程中提交任务到dispatch queue,不需要使用锁或其它同步机制。

不要在执行任务代码中调用 dispatch_sync 函数调度相同的queue,这样做会死锁这个queue。如果你需要dispatch到当前queue,需要使用 dispatch_async 函数异步调度

避免在提交到dispatch queue的任务中获得锁,虽然在任务中使用锁是安全的,但在请求锁时,如果锁不可用,可能会完全阻塞串行queue。类似的,并发queue等待锁也可能阻止其它任务的执行。如果代码需要同步,就使用串行dispatch queue。

虽然可以获得运行任务的底层线程的信息,最好不要这样做。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,360评论 5 461
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,195评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,543评论 0 323
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,420评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,242评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,224评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,626评论 3 384
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,301评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,589评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,660评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,423评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,286评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,662评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,951评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,237评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,613评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,804评论 2 335

推荐阅读更多精彩内容