iOS并发编程(三)-Dispatch Queues(GCD)

来源:互联网 发布:大数据改变教育 编辑:程序博客网 时间:2024/06/11 01:33

“最好的职业是能让你发挥所长,在现有的市场环境中游刃有余,实现个人抱负。”

提醒诸位同行,注意身体健康。

Dispatch Queues:

1.简介:

     GCD dispatch queues是执行任务的强大工具,允许你同步或异步地执行任意的代码block;

     原先使用单独线程执行的所有任务都可以替换为使用dispatch queues,而dispatch queues最大的优点在于使用简单而且高效;

     

     dispatch queue 是类似于对象的结构体,管理你提交给它的任务,而且都是先进先出的数据结构;因此queue中的任务总是以添加的顺序开始执行;

     GCD提供了几种dispatch queues,不过你也可以自己创建:

     1)串行:

     也称为private dispatch queue,每次只执行一个任务,按任务添加顺序执行;当前正在执行的任务在独立的线程中运行(不同任务的线程可能不同),dispatch queue管理了这些线程;通常串行queue只要用于对特定资源的同步访问;你可以创建任意数量的串行queues,虽然每个queue本身每次只能执行一个任务,但是各个queue之间是并发执行的;

     2)并发:

     也成为global dispatch queue,可以并发执行一个或多个任务,但是任务仍然是以添加到queue的顺序启动;每个任务运行与独立的线程中,dispatch queue管理所有线程;同时运行的任务数量随时都会变化,而且依赖于系统条件;你不能创建并发dispatch queues;相反应用只能使用三个已经定义好的全局并发queues;

     3)Main dispatch queue:

     全局可用的串行queue,在应用主线程中执行任务;这个queue与应用的run loop交叉执行;由于它运行在应用的主线程,main queue通常用于应用的关键同步点;虽然你不需要创建main dispatch queue,但你必须确保应用适当的回收;

     

     优点:

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

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

     

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

     

     dispatch queues的几个关键点:

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

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

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

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

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

     

     2.Queue相关的技术:

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

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

     2)Dispatch semaphore:

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

     3)Dispatch source:

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

     

     3.使用Block实现任务:

     Block可以非常容易地定义“自包含”的工作单元,尽管看上去非常类似于函数指针,block实际上由底层数据结构来表示,由编译器负责创建和管理;

     编译器对你的代码(和所有相关的数据)进行打包,封装为可以存在于堆中的格式,并在你的应用中各个地方传递;

     

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

     

     可以看一下testDispatchQueueUsing方法(下面的一个测试方法)中,block的简单使用-;

     

     设计Block时需要考虑一下关键指导方针:

     1)对于使用dispatch queue的异步Block,可以在Block中安全地捕获和使用父函数或方法中的scalar变量;但是Block不应该去捕获大型结构体或其他基于指针的变量,这些变量由Block的调用上下文分配和删除;在你的Block被执行时,这些指针引用的内存可能已经不存在;当然,你自己显示地分配内存(或对象),然后让Block拥有这些内存的所有权,是安全的;

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

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

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

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

     

     4.创建和管理Dispatch Queue:

     1)获取全局并发Dispatch Queue:

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

     

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

     

     三个优先级:

     DISPATCH_QUEUE_PRIORITY_DEFAULT

     DISPATCH_QUEUE_PRIORITY_HIGH

     DISPATCH_QUEUE_PRIORITY_LOW

     

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

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

-(void)testDispatchQueueUsing{    //    三:3 Block的简单使用    int x = 123;    int y = 456;        //Block declearation and assignment    void(^aBlock)(int) = ^(int z){        printf("%d-%d-%d",x,y,z);    };        aBlock(789);    //三:4-1)    dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);        //三:4-2)    dispatch_queue_t bQueue = dispatch_queue_create("com.example.MyQueue", NULL);        }

     

     2)创建串行Dispatch queue:

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

     

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

     

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

     

     3)运行时获得公共Queue:

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

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

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

     (3)使用dispatch_get_global_queue来获得共享的并发queue;

     

     4)Dispatch Queue的内存管理:

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

     和内存托管的Cocoa对象一样,通用的规则是如果你使用一个传递给你代码中的queue,你应该在使用前retain,使用完之后release;

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

     

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

     

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

     所有dispatch对象(包括dispatch queue)都允许你关联custom context data;使用dispatch_set_context和dispatch_get_context函数来设置和获取对象的上下文数据;

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

     这样,只要queue可获取,相应的上下文关联数据也就可以获取到;

     6)为Queue提供一个清理函数:

     在创建串行dispatch queue之后,可以附加一个finalizer函数,在queue被销毁之前执行自定义的清理操作;

     使用dispatch_set_finalizer_f 函数为queue指定一个清理函数,当queue的引用计数到达0时,就会执行该清理函数;

     你可以使用清理函数来解除queue关联的上下文数据,而且只有上下文指针不为NULL时才会调用这个清理函数(即如果没有设置关联的上下文,即便queue的引用计数到达0,该清理函数也是不会调用的);

     

     如下示例:自己提供myInitializeDataContextFunction 和 myCleanUpDataContextFunction 函数,用于初始化和清理上下文数据;

     

     问题:

     确实只有上下文对象不为NULL时,指定的清理函数才会调用;

     设置上下文之后,指定的清理函数接受到的参数为空,无法进行上下文对象的释放?//可以不指定清理方法 自己在dispatch queue释放之前 手动调用方法进行上下文的清理;

     

/** * 三:4-6) */void myFinalizerFunction(void * context){    //Now release the structure itself    free(context); }-(void)createQueue{    MyDataContext * data = [[MyDataContext alloc]init];    data.testProstr = @"flower";
     //create the queue and set the context data    self.serialQueue = dispatch_queue_create("com.senyint.flower", DISPATCH_QUEUE_SERIAL);        if (self.serialQueue) {
//设置上下文关联数据         dispatch_set_context(self.serialQueue, (__bridge void * _Nullable)(data));
//测试下 看是否能获取到         void * context = dispatch_get_context(self.serialQueue);        myInitializeDataContextFunction(context);                dispatch_set_finalizer_f(self.serialQueue, myFinalizerFunction);    }    for (int i = 0; i<1; i++) {        dispatch_async(self.serialQueue, ^{            NSLog(@"%d",i);        });    }    self.serialQueue = nil;}
//我们可以获取到已经设置好的关联上下文void myInitializeDataContextFunction(void * context){    MyDataContext * theData = (__bridge MyDataContext *)context;    NSLog(@"初始化:%@",theData.testProstr);//flower    }
这里的清理函数实际就是用于清理关联上下文的,因为只有上下文不为NULL时,这个清理函数在会在queue引用计数到达0的时候,调用。


     5.添加任务到Queue

     要执行一个任务,你需要将它dispatch到一个适当的dispatch queue,你可以同步或异步地dispatch一个任务,也可以单个或按组来dispatch;

     一旦进入到queue,queue会负责尽快地执行你的任务;

     

     1)添加单个任务到Queue:

     你可以异步或同步地添加一个任务到Queue,尽可能地使用dispatch_async或dispatch_async_f函数异步地dispatch任务;

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

     

     少数时候你可能希望同步地dispatch任务,以避免竞争条件或其他同步错误;

     使用dispatch_sync或dispatch_sync_f函数同步地添加任务到Queue,这两个函数会阻塞,直到相应的任务完成;

     

     注意:

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

     

-(void)testDispatch1{        dispatch_queue_t myCustomQueue = dispatch_queue_create("com.example.hua1", 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 work here.\n");    });        printf("Both blocks have completed.\n");    }

     2)任务完成时执行Completion Block:

     dispatch到queue中的任务,通常与创建任务的代码独立运行;在运行完成时,应用可能希望得到通知并使用任务完成的结果数据;

     dispatch queue允许你使用Completion Block;

     

     Completion Block是你dispatch到queue的另一段代码,在原始任务完成时自动执行;

     

     示例:

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

     

void average_async(int * data , size_t len , dispatch_queue_t queue , void(^block)(int)){    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{        int avg = average(data,len);        dispatch_async(queue, ^{            block(avg);        });    });    }int average(int * data , size_t len){    //test    return 10;}

     3)并发地执行Loop Iteration:

     如果你使用循环执行固定次数的迭代,并发dispatch queue可能会提高性能;

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

     

     }

     如果每次迭代执行的任务与其他迭代独立无关,而且循环迭代执行顺序也无关紧要的话,你可以调用dispatch_apply或dispatch_apply_f函数来替换循环;

     这两个函数为每次循环迭代将制定的block或函数提交到queue;当dispatch到并发queue时,就有可能同时执行多个循环迭代;

     

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

     

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

     

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

     

     示例:

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

     从log结果可以看出,使用dispatch_apply替换的方案,并不关注循环迭代的执行顺序,也就是无序的;

     

-(void)testDispatchApply{    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    dispatch_apply(10, queue, ^(size_t i) {        printf("%ld\n",i);//log 输出 可以看出 是无序的                if (i%2 == 1) {//由于执行是无序的 这种判断并不会使suspend和resume的调用平衡 请注意这只是测试            dispatch_suspend(queue);        }else{            dispatch_resume(queue);        }            });    }

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

     

     4)在主线程中执行任务:

     GCD提供了一个特殊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作为同步点;

     

     5)任务中使用Objective-C对象:

     GCD支持Cocoa内存管理机制,因此可以提交到queue的block中自由地使用OC对象;

     每个dispatch queue维护自己的autorelease pool确保释放autorelease对象,但是queue不保证这些对象实际释放的时间;

     在自动垃圾回收的应用中,GCD会在垃圾手机系统中注册自己的创建的每个线程;

     

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

     

     6.挂起和继续queue:

     我们可以暂停一个queue以阻止它执行block对象,使用dispatch_suspend函数挂起一个dispatch queue;

     使用dispatch_resume函数继续dispatch queue;

     调用dispatch_suspend会增加queue的引用计数,调用dispatch_resume则减少queue的引用计数;因此必须对应地调用suspend和resume函数;

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

          

     7.使用Dispatch Semaphore控制有限资源的使用:

     如果提交到dispatch queue中的任务需要访问某些有限资源,可以使用dispatch semaphore来控制同时访问这个资源的任务数量;

     dispatch semaphore和普通的信号量类似,唯一的区别是当资源可用时,需要更少的时间来获得dispatch semaphore;

     

     使用dispatch semaphore的过程如下:

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

     2)在每个任务中,调用dispatch_semaphore_wait来等待semaphore;

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

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

          

-(void)testDispatchSemaphore{    //    dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize()/2);//getdtablesize() 用来返回进程的文件描述表的最大项数,即进程打开文件的数目;        dispatch_semaphore_t fd_sema = dispatch_semaphore_create(3);    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    dispatch_apply(10, queue, ^(size_t i) {        //create the semaphore, specifying the inatial pool size                //wati for a free file descriptor        dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);        //操作具体有限资源        printf("有限资源的操作open/close %ld\n",i);//log 输出 可以看出 是无序的        dispatch_semaphore_signal(fd_sema);            });        }
有限资源的操作open/close 0有限资源的操作open/close 1有限资源的操作open/close 2有限资源的操作open/close 5有限资源的操作open/close 3有限资源的操作open/close 4有限资源的操作open/close 6有限资源的操作open/close 7有限资源的操作open/close 8有限资源的操作open/close 9

012 534 678 9 每三个一组的顺序确实确定的;

创建之后的有限信号量,在wait到时才可用,使用完之后需要释放;


     8.等待queue中的一组任务:

     Dispatch group用来阻塞一个线程,直到一个或多个任务完成执行;

     有时候你必须等待任务完成的结果,然后才能继续后面的处理;

     dispatch group 也可以替代线程join;

     

     基本的流程是设置一个组,dispatch任务到queue,然后等待结果;

     你需要使用dispatch_group_async函数,会关联任务到相关的组合queue;

     使用dispatch_group_wait等待一组任务完成;

     

     示例:

     示例中在dispatch_group_wait之后,通过dispatch_group_notify函数通知指定线程,当前线程任务已经结束;

     示例中当前线程是系统并发队列,所以任务的执行是无序的;

     可以通过dispatch_group_enter和dispatch_group_leave函数来实现任务的顺序调用;

     

-(void)testDispatchGroupQueue{    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    dispatch_group_t group = dispatch_group_create();        dispatch_group_enter(group);    dispatch_group_async(group, queue, ^{        //some asyncchronous work        printf("组内任务1\n");        dispatch_group_leave(group);    });        dispatch_group_enter(group);    dispatch_group_async(group, queue, ^{        //some asyncchronous work        printf("组内任务2\n");        dispatch_group_leave(group);    });        dispatch_group_enter(group);    dispatch_group_async(group, queue, ^{        //some asyncchronous work        printf("组内任务3\n");        dispatch_group_leave(group);    });        dispatch_group_enter(group);    dispatch_group_async(group, queue, ^{        //some asyncchronous work        printf("组内任务4\n");        dispatch_group_leave(group);    });        dispatch_group_enter(group);    dispatch_group_async(group, queue, ^{        //some asyncchronous work        printf("组内任务5\n");        dispatch_group_leave(group);    });        dispatch_group_enter(group);    dispatch_group_async(group, queue, ^{        //some asyncchronous work        printf("组内任务6\n");        dispatch_group_leave(group);    });            //do some other work while the tasks execute        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);    dispatch_group_notify(group, dispatch_get_main_queue(), ^{        //通知指定线程 当前线程的多个任务完成 这里通知的是主线程        printf("组内任务全部完成了\n");    });}

     9.Dispatch Queue和线程安全性:

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

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

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

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

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

     

总结:

这部分内容的学习,是一个循序渐进的过程,当前的内容,是再一次学习和实践之后的内容总结的内容,比较通俗了,关于GCD的内部实现可以看看这篇《深入理解GCD》,希望对大家有帮助。

0 0
原创粉丝点击