美文网首页
【iOS小结】GCD

【iOS小结】GCD

作者: WellsCai | 来源:发表于2017-11-09 13:39 被阅读0次

    一. GCD和多线程的介绍

    GCD

    GCD是异步执行任务的技术之一。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,比起使用NSThread类,performSelector系方法更为简洁,执行效率更高。

    dispatch_async(queue, ^{
            /*
             * 长时间处理
             * 例如AR用画像识别,数据库访问
             */
            
            // 长时间处理结束,主线程使用该处理结果
            dispatch_async(dispatch_get_main_queue(), ^{
                /*
                 * 只有在主线程可以执行的结果
                 * 例如用户界面更新
                 */
            });
        });
    
    多线程编程

    iOS,OS X操作系统启动应用程序后,首先便将包含在应用程序中的CPU命令列配置到内存中。CPU从应用程序指定的地址开始,一个一个地执行CPU命令列。但是,由于一个CPU一次只能执行一个命令,不能执行某处分开的并列的两个命令。


    通过CPU执行的CPU命令行.png

    “1个CPU执行的CPU命令列为一条无分叉路径”即为“线程”。如果存在多条即为多线程。在多线程中,1个CPU核执行多条不同路径上的不同命令。


    在多线程中执行CPU命令列.png
    现在基本上1个CPU核一次能够执行的CPU命令始终为1。对于单核来说,由于使用多线程的程序在某个线程和其他线程之间反复多次进行上下文切换,因此看上去好像1个CPU核能并列执行多个线程。对于多个CPU核来说,就是真的提供多个CPU核并行执行多个线程的技术。
    但是,多线程编程如果处理不好也容易发生各种问题:
    • 多个线程更新相同的资源会导致数据的不一致(数据竞争)
    • 停止等待事件的线程会导致多个线程相互持续等待(死锁)
    • 使用太多线程会消耗大量内存
      比如,应用程序在启动时,通过最先执行的线程,即“主线程”来描绘用户界面、处理触摸屏幕的事件等。如果在主线程进行长时间的处理,就会妨碍主线程的执行(阻塞)。在iOS的应用程序中,会妨碍主线程中的RunLoop的主循环的执行,从而导致不能更新用户界面、应用程序的画面长时间停滞等问题。


      多线程编程的优点.png

      使用多线程编程,在执行长时间的处理时仍可保证用户界面的响应。

    二. GCD的API

    Dispatch Queue

    开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。Dispatch Queue是执行处理的等待队列,其按照追加的顺序执行处理。


    通过Dispatch Queue执行处理.png

    在执行处理时,存在两种Dispatch Queue。一种是等待现在执行中处理的Serial Dispatch Queue,另一种是不等待现在执行中处理的Concurrent Dispatch Queue。


    Dispatch Queue的种类.png
    dispatch_queue_create

    通过GCD的API生成的Dispatch Queue。

    dispatch_queue_t mySerialDipathQueue = dispatch_queue_create("com.example.www", NULL);//默认Serial Dipath Queue
    

    Concurrent Dispatch Queue并行执行多个追加处理,而Serial Dispatch Queue同时只能执行1个追加处理。虽然Serial Dispatch Queue和Concurrent Dispatch Queue收到系统资源的限制,但用dispatch_queue_create函数可生成任意多个Dispatch Queue。
    当生成多个Serial Dispatch Queue时,各个Serial Dispatch Queue并行执行。虽然在1个Serial Dispatch Queue中同时只能执行一个追加处理,但如果将处理分别追加到4个Serial Dispatch Queue中,各个Serial Dispatch Queue执行1个,即为同时执行4个处理。


    多个Serial DispatchQueue.png

    如果生成Serial Dispatch Queue并追加处理,系统对于一个Serial
    Dispatch Queue就只生成并使用一个线程,如果生成2000个Serial
    Dispatch Queue,就生成2000个线程。如果过多使用多线程,就会消耗大量内存,引起大量上下文切换,大幅度降低系统的响应性能。

    当在避免多个线程更新相同资源导致数据竞争时使用Serial
    Dispatch Queue。且Serial
    Dispatch Queue的生成个数应该仅限所需的个数。比如更新数据库时一个表生成一个Serial
    Dispatch Queue。


    Serial Dispatch Queue的用途.png

    当想并行执行不发生数据竞争等问题的处理时,使用Concurrent Dispatch Queue。对于Concurrent Dispatch Queue,线程数由XNU内核决定和管理。

    //第一个参数推荐逆序全程域名,第二个参数指定串行或并行
    dispatch_queue_t myConcurrentDipathQueue = dispatch_queue_create("com.example.www", DISPATCH_QUEUE_CONCURRENT);
    //在队列中追加任务 
    dispatch_async(myConcurrentDipathQueue, ^{
         NSLog(@"myConcurrentDipathQueue");
    });
    //需要程序员负责释放
    dispatch_release(myConcurrentDipathQueue);
    

    Dispatch Queue像Objective-C的引用计数管理一样,需要通过dispatch_retain函数和dispatch_release函数的引用计数来管理内存。在dispatch_async函数中追加Block到Dispatch Queue后,即使立即释放Dispatch Queue,该Dispatch Queue由于被Block所持有也不会被废弃,因此Block能够执行。Block执行结束后释放Dispatch Queue,这时谁都不持有Dispatch Queue,因此它被废弃。

    Main Dispatch Queue/Global Dispatch Queue

    Main Dispatch Queue和Global Dispatch Queue是系统提供的,不用我们主动去生成。

    Main Dispatch Queue
    Main Dispatch Queue是在主线程中执行的Dispatch Queue。因为主线程只有1个,所以Main Dispatch Queue自然就是Serial Dispatch Queue。追加到Main Dispatch Queue的处理在主线程的RunLoop中执行。由于在主线程执行,因此要将用户界面的界面更新等一些必须在主线程中执行的处理追加到Main Dispatch Queue使用。

    Global Dispatch Queue
    Global Dispatch Queue是所有应用程序都能够使用的Concurrent Dispatch Queue。没有必要通过dispatch_queue_create 函数逐个生成Concurrent Dispatch Queue。只要获取Global Dispatch Queue使用即可。
    Global Dispatch Queue有4个执行优先级。通过XNU内核管理用于Global Dispatch Queue的线程,将各自使用的Global Dispatch Queue的执行优先级作为线程的执行优先级使用。

    Dispatch Queue的种类.png

    对于Main Dispatch Queue和Global Dispatch Queue,不用我们主动地进行dispatch_retain和dispatch_release(内部已实现)。

    //在默认优先级的Global Dispatch Queue中执行Block
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         /*
          * 可并行执行的处理
          */
            
         //在Main Dispatch Queue中执行Block
         dispatch_async(dispatch_get_main_queue(), ^{
              /*
               * 只能载主线程执行的处理
               */
          });
    });
    

    总结:
    dispatch_sync:同步任务无论在自定义串行队列、自定义并行队列、主队列(当前线程为主线程时会出现死锁)、全局队列 执行任务时,都不会创建子线程,而是在当前线程中串行执行;
    dispatch_async:异步任务无论在自定义串行队列、自定义并行队列(主队列除外,主队列下,任务会在主线中串行执行)、全局队列 执行任务时,都会创建子线程,并且在子线程中执行;

    dispatch_set_target_queue

    dispatch_queue_create函数生成的Dispatch Queue 不管是Serial Dispatch Queue还是Concurrent Dispatch Queue,都使用与默认优先级Global Dispatch Queue相同执行优先级的线程。变更生成的Dispatch Queue的执行优先级要使用dispatch_set_target_queue函数。

    dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.www", NULL);
    dispatch_queue_t globalDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    
    //将mySerialDispatchQueue的执行优先级改成和globalDispatchQueue执行优先级一样
    dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueue);
    

    将Dispatch Queue 指定为dispatch_set_target_queue 函数的参数,不仅可以变更Dispatch Queue的执行优先级,还可以作成Dispatch Queue的执行阶层。比如,在必须将不可并行执行的处理追加到多个Serial Dispatch Queue中时,如果使用dispatch_set_target_queue 函数将目标指定为某一个Serial Dispatch Queue,即可防止并行执行。

    dispatch_after

    想在指定时间后处理任务,可使用dispatch_after函数来实现。

    //常用DISPATCH_TIME_NOW,从现在开始
    //ull unsigned long long, NSEC_PER_SEC  秒(NSEC_PER_MSEC  毫秒)
    //也可以用dispatch_walltime ,主要用于指定时间点
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
    
    dispatch_after(time, dispatch_get_main_queue(), ^{
         NSLog(@"waited at least three seconds");
    });
    

    dispatch_after函数不是在指定的时间后执行处理,而是在指定时间追加处理到Dispatch Queue。因为Main Dispatch Queue 在主线程的RunLoop中执行,所以在每隔1/60秒执行的RunLoop中,Block最快在3秒后执行,最慢在3秒+1/60后执行,并且在Main Dispatch Queue 中有大量处理追加或主线程的处理本身有延迟时,这个时间会更长。

    Dispatch Group

    在追加到Dispatch Queue中的多个处理全部结束后想执行结束处理时,如果只使用一个Serial Dispatch Queue,只要将想执行的处理全部追加到该Serial Dispatch Queue中并在最后追加结束处理。如果是使用Concurrent Dispatch Queue或同时使用多个Dispatch Queue,就得使用
    Dispatch Group。

    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, ^{NSLog(@"blk0");});
    dispatch_group_async(group, queue, ^{NSLog(@"blk1");});
    dispatch_group_async(group, queue, ^{NSLog(@"blk2");});
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done");});
    dispatch_release(group);
    

    也可以使用dispatch_group_wait函数仅等待全部处理执行完毕。

    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, ^{NSLog(@"blk0");});
    dispatch_group_async(group, queue, ^{NSLog(@"blk1");});
    dispatch_group_async(group, queue, ^{NSLog(@"blk2");});
    
    long result = dispatch_group_wait(group, time);
    
    if (result == 0) {
            //属于Dispatch Queue的全部处理执行完毕
        }else{
            //属于Dispatch Queue的某一个处理还在执行
        }
    dispatch_release(group);
    

    当等待时间time为DISPATCH_TIME_FOREVER,返回结果恒为0。这里的等待意味着一旦调用dispatch_group_wait,该函数就处于调用的状态而不返回(到达等待时间才会返回),即执行dispatch_group_wait函数的现在的线程停止。

    dispatch_barrier_async

    dispatch_barrier_async函数会等待追加到Concurrent Dispatch Queue上的并行执行的处理全部结束后,再将指定的处理追加到该Concurrent Dispatch Queue中。然后在由dispatch_barrier_async函数追加的处理执行完毕后,Concurrent Dispatch Queue才恢复为一般的动作,追加到该Concurrent Dispatch Queue的处理又开始并行执行。

    Dispatch_barrier_async函数的处理流程.png

    比如在访问数据库时候,在多个读取操作中插入写入操作。
    在blk4_for_reading和blk5_for_reading处理之间执行写入处理,并将写入的内容读取blk5_for_reading处理以及之后的处理中。

    dispatch_queue_t queue = dispatch_queue_create("com.example.www", DISPATCH_QUEUE_CONCURRENT);
        
    dispatch_async(queue, blk1_for_reading);
    dispatch_async(queue, blk2_for_reading);
    dispatch_async(queue, blk3_for_reading);
    dispatch_async(queue, blk4_for_reading);
        
    //加入写入处理,后面读取的内容是该结果
    dispatch_barrier_async(queue, blk_for_writing);
        
    dispatch_async(queue, blk5_for_reading);
    dispatch_async(queue, blk6_for_reading);
    dispatch_async(queue, blk7_for_reading);
    dispatch_async(queue, blk8_for_reading);
    

    使用Concurrent Dispatch Queue 和 dispatch_barrier_async函数可实现高效率的数据库访问和文件访问。

    dispatch_sync

    dispatch_async的 async意味着非同步,就是将指定的Block非同步地追加到指定的Dispatch Queue中。dispatch_async函数不做任何等待。

    Dispatch_async函数的处理流程.png

    dispatch_async的 sync意味着同步,就是将指定的Block同步地追加到指定的Dispatch Queue中。dispatch_async函数会一直等待。

    Dispatch_sync函数的处理流程.png

    一旦调用dispatch_async函数,在指定的处理执行结束之前,该函数不会返回(类似dispatch_group_wait)。dispatch_asyn常用于非主线程。例如在主线程执行以下源码会造成死锁。

    dispatch_sync(dispatch_get_main_queue(), ^{NSLog(@"hello")};);
    

    该源码在主线程中执行指定的Block,并等待其执行结束。而其实在主线程中正在执行这些源代码,所以无法执行追加到Main Dispatch Queue的Block。下面的例子也是同理:

    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
         dispatch_sync(queue, ^{NSLog(@"hello");});
    });
    

    在Serial Dispatch Queue也会引起相同的问题:

    dispatch_queue_t queue = dispatch_queue_create("com.example.www", NULL);
        dispatch_async(queue, ^{
            dispatch_sync(queue, ^{NSLog(@"hello");});
        });
    

    所以在使用dispatch_sync函数等同步等待处理执行的API时,要深思熟虑。

    dispatch_apply

    dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API。该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_apply(10, queue, ^(size_t index){
        NSLog(@"%zu",index);
    });
    NSLog(@"done");
    //输出结果:  4 1 6 8 2 9 0 5 3 7 done
    

    因为在Global Dispatch Queue中执行处理,所以各个处理执行时间不定,但是最后一定是输出done,因为dispatch_apply函数会等待全部结果执行结束。
    另外,由于dispatch_apply与dispatch_sync函数相同,会等待处理执行结束,也推荐在dispatch_async中非同步地执行dispatch_apply函数。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //在global queue中非同步执行
    dispatch_async(queue, ^{
        dispatch_apply([array count], queue, ^(size_t index){
            /*
             * 并列处理包含在NSArray对象的全部对象
             */
        });
        
        //等待dispatch_apply函数中的处理全部执行结束,跳转到main queue中非同步执行
        dispatch_async(dispatch_get_main_queue(), ^{
            /*
             * 用户界面更新等
             */
        });
    });
    
    dispatch_suspend/dispatch_resume

    当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理。这种情况下,只要挂起Dispatch Queue即可,当可以执行时再恢复。

    //挂起指定的queue
    dispatch_suspend(queue);
    
    //恢复指定的queue
    dispatch_resume(queue);
    

    这些函数对已经执行的处理没有影响。挂起后,追加到Dispatch Queue中但尚未执行的处理在此之后停止执行。恢复后使得这些处理能够继续执行。

    Dispatch Semaphore

    当并行执行的处理更新数据时,会产生数据不一致的情况,虽然使用Serial Dispatch Queue和dispatch_barrier_async函数可以避免这类问题。但是要想进行更细粒度的排他控制就要使用Dispatch Semaphore。
    Dispatch Semaphore是持有计数的信号,该计数是多线程编程中的计数类型信号。在Dispatch Semaphore中,使用计数来实现该功能。计数0时等待,计数1或大于1时,减去1而不等待。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    /*
     * 生成计数初始值1的Dispatch Semaphore
     * 保证可访问array对象的线程只有1个
     */
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    NSMutableArray *array = [[NSMutableArray alloc] init];
    
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            /*
             *一直等待,直到Dispatch Semaphore的计数值达到大于等于1
             */
            
            //当Dispatch Semaphore的计数值大于等于1执行到这一步,
            //将计数值减1,并且执行返回。(返回值同dispatch_group_wait一样)
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            
            //所以只有1个线程能做该操作
            [array addObject:[NSNumber numberWithInt:i]];
            
            //将Dispatch Semaphore的计数值加1
            dispatch_semaphore_signal(semaphore);
        });
    }
    
    //只要有crate就要我们去release,类似Dispatch Group
    dispatch_release(semaphore);
    
    dispatch_once

    通过dispatch_once函数,即使在多线程环境下执行也保证线程安全。所以常用于生成单例对象。

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        /*
         * 初始化
         */
    });
    

    相关文章

      网友评论

          本文标题:【iOS小结】GCD

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