阅读 114

笔记整理:GCD

一、概述

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

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

  • 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

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

  • 执行顺序:任务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

作者:双鱼子曰1987

原文链接:https://www.jianshu.com/p/b6718e529944

文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐