美文网首页
笔记整理:GCD

笔记整理:GCD

作者: 双鱼子曰1987 | 来源:发表于2021-06-08 10:19 被阅读0次

    一、概述

    GCD是用纯C编写,效率很高;其内部自动维护一个线程池,自动管理线程的生命周期;其会利用CPU的多核特性。


    image.png

    二、GCD 基础

    1、GCD使用的核心是队列Queue 和 任务Block;

    • 任务可分为同步任务dispatch_sync 和 异步任务dispatch_async

    • 队列就是先进先出FIFO的数据结构,可以分为串行队列 和 并行队列
      串行队列中的任务是顺序执行,即one by one;并行队列的任务并行随机执行,无顺序。

      GCD提供默认队列:主线程队列dispatch_get_main_queue() 它是主线程上的串行队列;全局队列dispatch_get_global_queue(0,0) 它是全局的并行队列。

    • 同步和异步的区别有以下两点:
      1、任务的执行是否需要阻塞当前线程,同步会阻塞,异步则不会。
      2、是否具备开启新线程的能力。
      同步任务:不管在串行或并行队列,都只能在当前线程内执行任务和等待,不具备开启新线程的能力。
      异步任务:不管在串行或并行队列,一般都会到新线程中执行任务,具备开启新线程的能力。

    2、组dispatch_group —— 针对的是队列queue 和 任务task

    相当于在队列中设置一个里程碑任务,这个里程碑任务 必须等到队列中其他任务都完成,才会被触发。

    • 1、创建组的方式
    dispatch_group_t group =  dispatch_group_create();
    
    • 2、设置里程碑任务的方法
    dispatch_group_notify(group, queue, ^{
        // 里程碑任务
    });
    

    注意:wait方法,其阻塞当前线程

    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    执行,里程碑任务
    
    • 3、往group添加任务的方法
    dispatch_group_async(group, queue, ^{
        // 追加任务 
    });
    

    手动控制Group的进入和退出(enter和leave必须配对)

    // 进入group
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务 1
    
        // 当前任务退出group
        dispatch_group_leave(group);
    });
    

    3、栅栏dispatch_barrier_async —— 针对的是队列queue

    以栅栏函数调用为分界点,调用之前加入队列的任务组先执行,然后执行栅栏任务,最后执行调用之后加入队列的任务组。

    • 执行顺序:任务1/任务2 ---> 栅栏任务 ---> 任务3/任务4
    - (void)barrier {
        dispatch_queue_t queue = dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_async(queue, ^{
            // 追加任务 1
        });
    
        dispatch_async(queue, ^{
            // 追加任务 2
        });
        
        dispatch_barrier_async(queue, ^{
            // 追加任务 barrier
        });
        
        dispatch_async(queue, ^{
            // 追加任务 3
        });
        dispatch_async(queue, ^{
            // 追加任务 4
        });
    }
    

    4、dispatch_semaphore

    主要提供三个方法
    dispatch_semaphore_create(counts) 创建信号量,初始信号总量
    dispatch_semaphore_signal(semp) 信号量+1
    dispatch_semaphore_wait(semp, time) 信号量为0则阻塞线程,大于0则不会阻塞。

    • 使用场景1:保持线程同步,将异步任务转换为同步任务
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        
    dispatch_async(queue, ^{
        // 任务
        dispatch_semaphore_signal(semaphore);
    });
        
    // 阻塞线程,且只有执行完任务后,才会继续往下
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    • 使用场景2:保证线程安全,为线程加锁,即保证数据的准确性。

    5、GCD定时器 - dispatch source

    • 定时器创建流程:source_create -> set_timer -> set_event_handler -> resume
      官方的demo示例
    dispatch_source_t CreateDispatchTimer(uint64_t interval, uint64_t leeway, dispatch_queue_t queue, dispatch_block_t block) {
         // 创建source
         dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
           0, 0, queue);
         if (timer) {
            // 设置定时器的定时间隔、开始时间
            dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
            // 设置定时器的执行handler
            dispatch_source_set_event_handler(timer, block);
            // 唤起定时器
            dispatch_resume(timer);
         }
         return timer;
    }
    
    • 定时器暂停(这时候不能被释放,否则EXC_BAD_INSTRUCTION 崩溃)
    dispatch_suspend(_timer);
    _timer = nil; // 崩溃
    
    • 定时器销毁
    dispatch_source_cancel(_timer);
    _timer = nil; // OK
    

    6、其他常用接口

    • dispatch_once 保证代码之后执行一次。
    • dispatch_after 延迟几秒后执行任务。
    • dispatch_apply 快速迭代任务,即重复几次的执行同一个任务。
    dispatch_apply(runCounts, queue, ^(size_t index) {
            NSLog(@"%zd---%@",index, [NSThread currentThread]);
    });
    

    三、队列相关配置

    1、dispatch_queue_set_specific,把任意数据以键值对的形式关联到队列中

    void dispatch_queue_set_specific(
        dispatch_queue_t queue, //待设置标记的队列
        const void *key, //标记的键
        void *context, //标记的值。注意,这里键和值是指针,即地址,故context中可以放任何数据,但必须手动管理context的内存
        dispatch_function_t destructor //析构函数,但所在队列内存被回收,或者context值改变时,会被调用
    );
    
    • 使用场景:用来判断当前队列。
      使用Demo
    dispatch_queue_t queueA = dispatch_queue_create("queueA", NULL);
    dispatch_queue_t queueB = dispatch_queue_create("queueB", NULL);
    dispatch_set_target_queue(queueB, queueA);
     
    static int kQueueSpecific;
    CFStringRef queueSpecificValue = CGSTR("queueA"); 
    //这里使用CoreFoundation字符串,是因为ARC不会自动管理CoreFoundation对象的内存,dispatch_queue_set_specific的第三个参数(值)需要手动管理内存
     
    dispatch_queue_set_specific(
        queueA, 
        &kQueueSpecific, 
        (void *)queueSpecificValue,  //需要手动管理内存
        (dispatch_function_t)CFRelease //用CFRelease清理旧值
    ); //给queueA队列做标记
     
    dispatch_sync(queueB, ^{
        dispatch_block_t block = ^{ NSLog("No deadlock!"); };
        CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific); //根据键获取值
        if(retrievedValue){ //根据键找到了值,就说明包含在queueA目标队列中
            block();
        }else{ //没有包含在queueA中
            dispatch_sync(queueA, block);
        }
    })
    

    2、获取当前queue的标签 dispatch_queue_get_label(queue),一般用来判断两个队列是否相同。

    // 判断当前队列是否相等
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(queue)) {
        ...
    }
    

    3、dispatch_set_target_queue

    * @discussion
     * An object's target queue is responsible for processing the object. 
     *
     * When no quality of service class and relative priority is specified for a
     * dispatch queue at the time of creation, a dispatch queue's quality of service
     * class is inherited from its target queue. The dispatch_get_global_queue()
     * function may be used to obtain a target queue of a specific quality of
     * service class, however the use of dispatch_queue_attr_make_with_qos_class()
     * is recommended instead.
     *
     * Blocks submitted to a serial queue whose target queue is another serial
     * queue will not be invoked concurrently with blocks submitted to the target
     * queue or to any other queue with that same target queue.
     *
     * The result of introducing a cycle into the hierarchy of target queues is
     * undefined.
     *
     * A dispatch source's target queue specifies where its event handler and
     * cancellation handler blocks will be submitted.
     *
     * A dispatch I/O channel's target queue specifies where where its I/O
     * operations are executed. If the channel's target queue's priority is set to
     * DISPATCH_QUEUE_PRIORITY_BACKGROUND, then the I/O operations performed by
     * dispatch_io_read() or dispatch_io_write() on that queue will be
     * throttled when there is I/O contention.
     *
     * For all other dispatch object types, the only function of the target queue
     * is to determine where an object's finalizer function is invoked.
     *
     * In general, changing the target queue of an object is an asynchronous
     * operation that doesn't take effect immediately, and doesn't affect blocks
     * already associated with the specified object.
     *
     * However, if an object is inactive at the time dispatch_set_target_queue() is
     * called, then the target queue change takes effect immediately, and will
     * affect blocks already associated with the specified object. After an
     * initially inactive object has been activated, calling
     * dispatch_set_target_queue() results in an assertion and the process being
     * terminated.
     *
     * If a dispatch queue is active and targeted by other dispatch objects,
     * changing its target queue results in undefined behavior.
     *
     *
     * @param object
     * The object to modify.
     * The result of passing NULL in this parameter is undefined.
     *
     * @param queue
     * The new target queue for the object. The queue is retained, and the  
     * previous target queue, if any, is released. 
     * If queue is DISPATCH_TARGET_QUEUE_DEFAULT, set the object's target queue
     * to the default target queue for the given object type.
    void
    dispatch_set_target_queue(dispatch_object_t object,
            dispatch_queue_t _Nullable queue);
    

    应用1:改变queue的优先级与目标queue相同

    dispatch_queue_t serialQueue = dispatch_queue_create("com.oukavip.www",NULL);
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);
     
    // 第一个参数为要设置优先级的queue,第二个参数queue是参照物,
    // 既将第一个queue的优先级和第二个queue的优先级设置一样。
    dispatch_set_target_queue(serialQueue, globalQueue);
    

    应用2:
    一般都是把一个任务放到一个串行的queue中,如果这个任务被拆分了,被放置到多个串行的queue中,但实际还是需要这个任务同步执行,那么就会有问题,因为多个串行queue之间是并行的。

    那该如何是好呢?就可以使用dispatch_set_target_queue了。

    如果将多个串行的queue使用dispatch_set_target_queue指定到了同一目标,那么多个串行queue在目标queue上就是串行执行的,不再是并行执行。

    dispatch_get_current_queue可能导致死锁
    
    dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue", DISPATCH_QUEUE_SERIAL);
        
    dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue3 = dispatch_queue_create("test.3", DISPATCH_QUEUE_SERIAL);
    
    dispatch_set_target_queue(queue1, targetQueue);
    dispatch_set_target_queue(queue2, targetQueue);
    dispatch_set_target_queue(queue3, targetQueue);
    
    
    dispatch_async(queue1, ^{
        NSLog(@"1 in");
        [NSThread sleepForTimeInterval:3.f];
        NSLog(@"1 out");
    });
    
    dispatch_async(queue2, ^{
        NSLog(@"2 in");
        [NSThread sleepForTimeInterval:2.f];
        NSLog(@"2 out");
    });
    dispatch_async(queue3, ^{
        NSLog(@"3 in");
        [NSThread sleepForTimeInterval:1.f];
        NSLog(@"3 out");
    });
    
    // 1 in 1 out  , 2 in 2 out , 3in 3 out
    

    4、dispatch_suspenddispatch_resume

    简单理解,就是可以暂停、恢复队列上的任务。
    需要注意的是,针对的是那些已经放入队列中,但是还没有被执行的任务,已经正在执行的会继续执行。


    GCD注意事项:

    造成死锁的情况:

    • 1、「异步执行+串行队列」嵌套「同步执行+串行队列」
    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serialQueue, ^{
        // task 1
        dispatch_sync(serialQueue, ^{
            // task 2
        });    
    });  
    
    • 2、「同步执行+串行队列」嵌套「同步执行+串行队列」
    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serialQueue, ^{
        // task 1
        dispatch_sync(serialQueue, ^{
            // task 2
        });    
    });  
    
    • 3、主线程中,「同步执行」到 「主线程队列」,很容易卡主主线程。
    dispatch_sync(dispatch_get_main_queue(), ^{
        // block
    });    
    
    • dispatch_get_current_queue 容易导致死锁
      在iOS6之后是弃用的,苹果只推荐在打印中使用,原因是容易导致死锁。

    安全的主线程判断:

    + (BOOL)isMainQueue {
        static const void* mainQueueKey = @"mainQueue";
        static void* mainQueueContext = @"mainQueue";
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, mainQueueContext, nil);
        });
        return dispatch_get_specific(mainQueueKey) == mainQueueContext;
    }
    

    主线程中也不绝对安全的 UI 操作


    iOS 多线程:『GCD』详尽总结
    深入理解 GCD
    Concurrent Programming: APIs and Challenges
    深入理解GCD之dispatch_queue

    相关文章

      网友评论

          本文标题:笔记整理:GCD

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