GCD 基本

介绍

中央调度(GCD)由语言特征、运行时库和系统改善组成,对运行iOS和OSX系统的多核硬件并行执行代码提供了系统的、全面的支持。BSD子系统,CoreFoundation和Cocoa的API都已扩展来使用这项改善技术来帮助系统和应用程序运行得更快速更有效更响应。考虑到一个应用程序能够有效地使用多核是多么的困难,更不用说在有不同核数的计算设备里和其他应用程序竞争。在系统级工作的GCD可以根据所有正在运行的程序,以最平衡的的方式分配它们有效的系统资源。GCD不光支持在Unix类操作系统的同步异步执行代码,还可以与文件描述符、Mach端口、Unix信号和定时器工作。

在使用Objc编译器构建App时,所有的dispatch objects(调度对象)都是Objc对象。所以,当ARC(Automatic reference counting)开启时,dispatch objects会像Objc对象一样被编译器自动在适当的地方retain和release。当ARC关闭时,使用dispatch_retain和dispatch_release(而不是retain和release)管理内存。

基本概念

  • **串行并行(Serial & Concurrent)**串行是指同一时间只执行一个任务,并行是指同一时间能执行多个任务。串行执行完前一个任务后才能开始执行后一个任务,严格保证任务之间的执行顺序。并行开始执行一个任务和其他并行任务无关,任务之间的执行顺序也是任意随机。
  • **同步异步(Synchronize & Asynchronize)**同步异步是为了描述任务发起者和任务执行者之间的等待关系。同步是指,任务发起者必须要等到任务执行者执行完任务之后再继续做自己的事儿。异步是指,任务发起者不用等到任务执行者执行完任务之后就可以继续做自己的事儿。线程上的表现,同步就是阻塞挂起休眠,异步就是返回继续执行。
  • **临界区(Critical Area)**临界区是指一个不能同时被多个线程访问、当有线程进入执行其他线程必须等待的代码块。
  • **竞态条件(Race Condition)**静态条件是某任务不能屏蔽被多线程无序执行带来的副作用(执行结果对顺序敏感)。
  • **死锁(Dead Lock)**死锁是指多线程由于竞争造成所有相关线程都被阻塞而程序无法继续执行。

并发与并行

宏观上并发与并行都是同时执行。微观上并发同一时间只执行一个任务,但执行适当时间后暂停当前任务,切换到新任务再执行适当时间;并行确实真真正正的同一时间都在执行多个任务。其差别在于,系统能够使用的核数目。单核的确只能执行一个任务为了支持多线程才用并发,多核就能够执行多个任务便可以并行。

队列

dispatch queues(调度队列)是GCD提供用来提交任务并执行的轻量级对象。dispatch queues以FIFO(First In First Out)的顺序开始执行任务。系统管理线程池用来处理分配线程执行提交到dispatch queues的任务。所有dispatch queues都是线程安全的,可以在多线程中访问。GCD提供1个串行队列(主队列)和4个并行队列(全局队列:高、默认
低、后台),也可以创建自定义的串行或者并行队列。

**串行队列(Serial Queue)**的任务串行执行(以FIFO开始和结束),因此不会出现竞争资源的临界区访问。

  • dispatch_get_main_queue()主队列由系统自动创建并和主线程关联唯一能访问UI的队列
  • dispatch_queue_create(“Serial”,DISPATCH_QUEUE_SERIAL)

**并行队列(Concurrent Queue)**的任务并行执行(以FIFO开始)。旧任务何时完成,新任务何时开始,某时刻多少个任务在执行完全取决于GCD。

  • dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0)
  • dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)
  • dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,0)
  • dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0)
  • dispatch_queue_create(“Concurrent”,DISPATCH_QUEUE_CONCURRENT)

dispatch_queue_create自定义队列创建时可以用字符串当标签用来在XCode调试、Instruments检测,Crash报告中标识自己。

dispatch_set_target_queue一个对象的目标队列决定了最终在那个队列里处理该对象(调度队列Dispatch Queue、调度源Dispatch Sources、调度I/O管道)。对调度队列而言,如果有目标队列,则其优先级继承自目标队列。使用目标队列可以实现很多满足特定需求的队列体系。动态改变任务优先级,动态改变任务同异步。比如任务组里的任务要同步,且与其他任务组的任务也同步:

dispatch_queue_t targetQueue = dispatch_queue_create("com.Banana.Demo", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serial1 = dispatch_queue_create("Serial1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serial2 = dispatch_queue_create("Serial2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serial3 = dispatch_queue_create("Serial3", DISPATCH_QUEUE_SERIAL);

dispatch_set_target_queue(serial1, targetQueue);
dispatch_set_target_queue(serial2, targetQueue);
dispatch_set_target_queue(serial3, targetQueue);

dispatch_async(serial1, ^{
    // Task 1_1
});
dispatch_async(serial1, ^{
    // Task 1_2
});

dispatch_async(serial2, ^{
    // Task 2_1
});
dispatch_async(serial2, ^{
    // Task 2_2
});

dispatch_async(serial3, ^{
    // Task 3_1
});
dispatch_async(serial3, ^{
    // Task 3_2
});

管理调度对象

GCD提供调度对象接口让程序管理诸如内存管理、暂停恢复、定义语境和记录日志的功能。

内存管理:dispatch_retain和dispatch_release(ARC不用),dispatch_set_finalizer_f(调度对象销毁前用于释放相关连的资源)。
暂停恢复:dispatch_resume和dispatch_suspend。
定义语境:dispatch_set_context和dispatch_get_context。
记录日志:dispatch_debug。

dispatch_set_context参数context是一个可以指向任意的void*,为队列设置任意类型数据,在合适时取出使用。GCD是主要用C编写的底层库,对于C开辟的内存和Cpp创建的对象本身有Coder自己管理内存,但Objc在ARC环境中就需要接管Objc对象的内存管理以免在获取context时已经释放。

typedef struct TestC {
    int number;
}TestC;
void finalizerC(void *context) {
    TestC *testC = (TestC *)context;
    NSLog(@"End C: %d", testC->number);
    free(testC);
}

@interface TestObjC : NSObject
@property(assign, nonatomic) int number;
@end
@implementation TestObjC
- (void)dealloc {
    NSLog(@"%@", NSStringFromSelector(_cmd));
}
@end
void finalizerObjC(void *context) {
    TestObjC *testObjC = (__bridge_transfer TestObjC *)(context);
    NSLog(@"End ObjC: %d", testObjC.number);
}

- (void)test {
    dispatch_queue_t queueC = dispatch_queue_create("com.Banana.TestC", DISPATCH_QUEUE_SERIAL);
    dispatch_set_context(queueC, malloc(sizeof(TestC)));
    dispatch_set_finalizer_f(queueC, finalizerC);
    dispatch_async(queueC, ^{
        TestC *testC = dispatch_get_context(queueC);
        NSLog(@"Start C: %d", testC->number);
        testC->number = 1;
    });
    
    dispatch_queue_t queueObjC = dispatch_queue_create("com.Banana.TestObjC", DISPATCH_QUEUE_SERIAL);
    dispatch_set_context(queueObjC, (__bridge_retained void *)[[TestObjC alloc] init]);
    dispatch_set_finalizer_f(queueObjC, finalizerObjC);
    dispatch_async(queueObjC, ^{
        TestObjC *testObjC = (__bridge TestObjC *)dispatch_get_context(queueObjC);
        NSLog(@"Start ObjC: %d", testObjC.number);
        testObjC.number = 1;
    });
}
  • __bridge:只转换类型(Ojbc<->CF),不改所有权;
  • __bridge_retained(即CFBridgingRetain)转换类型(Ojbc->CF),并从ARC获取所有权,后续负责管理对象内存;
  • __bridge_transfer(即CFBridgingRelease)转换类型(CF->Objc),并移交所有权到ARC,不再负责管理对象内存。

执行

  • sync所有的dispatch-sync方法同步提交任务到队列并阻塞直到任务结束。
  • async所有的dispatch-async方法异步提交任务到队列并立即返回继续执行。
  • dispatch_after异步提交定时任务并立即返回继续执行。
  • dispatch_apply同步提交多个任务并阻塞直到所有任务结束。任务间根据目标队列(Target Queue)同步(意义不大)或者异步执行。
  • dispatch_group_wait阻塞当前线程直到所有提交的任务结束或者超时。
  • dispatch_semaphore_wait阻塞当前线程直到获取一个信号量。
  • dispatch_barrier_sync同步提交一个屏障任务并阻塞直到任务结束。

以上方法中所有的同步方法在使用时都需要认真考虑以避免出现死锁。串行队列就很容易出戏:

SyncMethodA(SerialQueue, ^{
    SyncMethodB(SerialQueue, ^{
        // The Task
    });
});

Sync为什么比较容易出现死锁?我个人认为因为封装。方法封装了操作,调用时(该方法,没有源代码,没有API说明,或者Coder根本就没注意或者懒得注意或者开始注意后来不小心忘记)不知道方法里面(或者里面的里面的里面)是否嵌套了同步,如果继续同步调用该方法就很有可能死锁(即使不是在同一个队列)。比较保险的方法就是在符合语义的情况下尽可能使用异步方法,对于在异步部分之后需要同步的内容可以写在异步任务里顺序执行。

单例

单例模式的常见问题是,单例常常在多线程中同时被多个对象访问而线程不安全。dispatch_once整个程序生命周期里,以线程安全的方式执行且仅执行一次任务,很适合单例初始化:

+ (instancetype)sharedname {
    static class *_sharedname = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedname = initializer;
    });
    
    return _sharedname;
}

程序可以提交多个任务(任务可以是同步也能是异步,即使在不同队列中)到调度组并跟踪它们直到都结束或者超时。提交到任务组有2种方式:

  • dispatch_group_async
  • dispatch_group_enter和dispatch_group_leave(必须成对调用)

任务组结束的通知任务也有同步dispatch_group_wait和异步dispatch_group_notify2种方式。

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, globalQueue, ^{
    // Task1
});
dispatch_group_enter(group);
// Task 2
dispatch_group_leave(group);

dispatch_group_notify(group, mainQueue, ^{
    // Async Notify
});

dispatch_time_t then = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC));
dispatch_group_wait(group, then);
// Sync Notify

信号量

调度信号量是传统计数信号量的有效实现。调度信号量只在调用线程需要被阻塞时才产生内核调用。dispatch_semaphore_signal创建并设置初始信号量,dispatch_semaphore_signal产生一个信号量并唤醒等待信号量的线程,dispatch_semaphore_wait等待阻塞线程直到有一个信号量消耗并恢复执行。

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_time_t then = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
    dispatch_async(queue, ^{
        // Task1
        if (!dispatch_semaphore_wait(semaphore, then)) {
            NSLog(@"YES 1");
            sleep(1.0);
            dispatch_semaphore_signal(semaphore);
        } else {
            NSLog(@"NO 1");
        }
    });
    dispatch_async(queue, ^{
        // Task1
        if (!dispatch_semaphore_wait(semaphore, then)) {
            NSLog(@"YES 2");
            sleep(1.0);
            dispatch_semaphore_signal(semaphore);
        } else {
            NSLog(@"NO 2");
        }
    });
    dispatch_async(queue, ^{
        // Task1
        if (!dispatch_semaphore_wait(semaphore, then)) {
            NSLog(@"YES 3");
            sleep(1.0);
            dispatch_semaphore_signal(semaphore);
        } else {
            NSLog(@"NO 3");
        }
    });

屏障

在线程同步技术中提到过内存屏障同步内存读写操作,GCD使用队列屏障在并行队列里创建一个同步机会,等待已提交的任务执行结束,禁止没提交的任务开始执行。使得执行队列屏障任务时不会有其他任务执行,一旦执行完成,并行队列恢复之前的状态和行为。GCD支持同步提交dispatch_barrier_sync和异步dispatch_barrier_async提交2种方式。

- (void)setSafeObject:(id)object forKey:(NSString *)key {
    key = [key copy];
    dispatch_barrier_async(self.ioQueue, ^{
        if (key && object) {
            [_dic setObject:object forKey:key];
        }
    });
    [key release];
}

- (id)getSafeObjectForKey:(NSString *)key {
    __block id result = nil;
    dispatch_sync(self.ioQueue, ^{
        result = [_dic objectForKey:key];
    });
    return result;
}

延伸阅读


GCD 基本
https://hllovesgithub.github.io/2015/10/18/2015-10-18-GCD基本/
作者
Hu Liang
发布于
2015年10月18日
许可协议