GCD多线程开发

概述

GCD(Grand Central Dispatch)是苹果在iOS4.0引入的多线程编程技术。可以替代之前的NSThread, NSOperation的做法。本身用纯C语言编写,轻量级、高性能。 而且GCD采用block作为执行主体,可以利用block的很多优势。当然,如果需要也可以脱离block使用。 用GCD的优势在于代码更为简洁,运行效率更高,也更有利于做平行计算。

Dispatch Queues

Dispatch Queues是GCD的基本概念,可以接受任务,按并发或串行来执行。 并发可以根据系统负载那样来合适地并发。

一般有这三种:

  1. The main queue: 主线程中执行。使用dispatch_get_main_queue()来获得。这是串行队列。
  2. Global queues: 并发队列。可以调用dispatch_get_global_queue函数活得。
  3. 用户队列: 用函数 dispatch_queue_create 创建的队列. 这些队列是串行的。

常用的就是下面几种使用方法:

后台执行

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
	NSLog(@"run in background ...");

});

其中第一个参数是是控制线程的优先级,GCD支持的优先级有:

  1. DISPATCH_QUEUE_PRIORITY_HIGH:高优先级
  2. DISPATCH_QUEUE_PRIORITY_DEFAULT:默认优先级
  3. DISPATCH_QUEUE_PRIORITY_LOW:低优先级
  4. DISPATCH_QUEUE_PRIORITY_BACKGROUND:最低的优先级,可以用于最小避免IO操作对系统的影响。

第二个参数是为将来做考虑的,目前只需要写0。

主线程执行

dispatch_async(dispatch_get_main_queue(), ^{
	NSLog(@"run in main ...");
});

一次性执行

使用dispatch_once,这函数不仅意味着代码仅会被运行一次,而且还是线程安全的,可以不用@synchronized之类的来防止多个线程不同步的问题。这个很适合用来写单例。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
	NSLog(@"run once ...");
});

延迟1秒执行

double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
	NSLog(@"delay with %f", delayInSeconds);
});

dispatch_time是用来创建一个dispatch_time_t数据结构,第一个参数可以传入DISPATCH_TIME_NOW或者DISPATCH_TIME_FOREVER,分别代表当前时间和无限大的时间。 NSEC_PER_SEC代表一秒钟包含多少纳秒,NSEC_PER_MSEC代表一毫秒包含多少纳秒;

Dispatch Objects

使用

Dispatch Objects的作用就是用来监听一些特定的底层系统事件。目前GCD支持的事件类型有:

  • 计时器类型
  • Unix信号量通知
  • 文件和socket的变化
  • 进程状态的变化
  • Mach port事件
  • 自定义dispatch sources

如使用内建事件读取标准输入:

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建一个内建事件,从标准输入读取数据
dispatch_source_t stdinSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, STDIN_FILENO, 0, globalQueue);
// 设置事件句柄
dispatch_source_set_event_handler(stdinSource, ^{
	char buf[1024];
	// 读取标准输入
	int len = read(STDIN_FILENO, buf, sizeof(buf));
	if(len > 0)
		NSLog(@"Got data from stdin: %.*s", len, buf);
});
// dispatch source启动时默认状态是挂起的,创建完毕之后得主动恢复:
dispatch_resume(stdinSource);

注意某些情况是需要dispatch_source_set_cancel_handler取消dispatch sources,以关闭文件描述符的。 TODO:试验此类情况)

计时器

虽然有简单的NSTimer类可以定时或延时操作,但是NSTimer是必须要在run loop已经启用的情况下使用的,否则无效。但只有主线程是默认启动run loop的。 我们不能保证自己写的方法不会被人在异步的情况下调用到,所以有时使用NSTimer不是很保险。那么用GCD有另外一套做法可以取代:

int interval = 2;
// 这个参数告诉系统我们需要计时器触发的精准程度,0的话是努力保持最大精度;如果能接受误差为60秒,则改为60。
int leeway = 0;

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 创建计时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
if (timer) {
	dispatch_source_set_timer(timer, dispatch_walltime(DISPATCH_TIME_NOW, NSEC_PER_SEC * interval), interval * NSEC_PER_SEC, leeway);
	dispatch_source_set_event_handler(timer, ^{
		NSLog(@"dispatch timer ..");
	});
	// 恢复挂起
	dispatch_resume(timer);
}

并行处理

GCD用来做并行处理很容易。比如下面的例子,我创建一个小数组,每个数组中的每个对象都做了10000000次的乘法计算:

单线程处理

- (void)test
{
    NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];

    NSArray *array = [self createTestArray];
    for (id obj in array) {
        [self doSomethingWithObj:obj];
    }

    NSTimeInterval endTime = [[NSDate date] timeIntervalSince1970];
    NSLog(@"cost time: %f", (endTime - startTime));
}

- (NSArray*)createTestArray
{
    NSMutableArray *array = [NSMutableArray array];
    for (int i=0; i<10; i++) {
        [array addObject:@(i)];
    }
    return array;
}

- (void)doSomethingWithObj:(id)obj
{
    NSLog(@"print objc: %@", obj);
    for (int i=0; i<10000000; i++) {
        int value = [(NSNumber*)obj intValue];
        int result = value * value;
        result++;
    }
}

结果:

cost time: 2.250432

并行处理

修改处理数组的函数为并发方式,这里用了dispatch_apply,不过注意这个是同步操作,会阻塞当前的线程。

- (void)test
{
    NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];

    NSArray *array = [self createTestArray];
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply([array count], queue, ^(size_t index){
        [self doSomethingWithObj:[array objectAtIndex:index]];
    });

    NSTimeInterval endTime = [[NSDate date] timeIntervalSince1970];
    NSLog(@"cost time: %f", (endTime - startTime));
}

结果:

cost time: 0.660977

速度快了3.4倍!效果确实明显。

dispatch group

上述的写法也可以用dispatch group来代替。dispatch group可以用来将多个block组成一组,等这些block都完成之后才触发其他操作。

- (void)test
{
    NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
    
    NSArray *array = [self createTestArray];
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 创建一个组:
    dispatch_group_t group = dispatch_group_create();
    for(id obj in array) {
        // 每个对象都做异步操作
        dispatch_group_async(group, queue, ^{
            [self doSomethingWithObj:obj];
        });
    }
    // 所有操作执行完之后才执行:
    dispatch_group_notify(group, queue, ^{
        [self doSomethingWithArray:array];
        NSTimeInterval endTime = [[NSDate date] timeIntervalSince1970];
        NSLog(@"cost time: %f", (endTime - startTime));
    });
}

信号量

信号量跟普通线程的信号量概念是一样的:

信号量是一个整形值并且具有一个初始计数值,并且支持两个操作:信号通知和等待。当一个信号量被信号通知,其计数会被增加。当一个线程在一个信号量上等待时,线程会被阻塞(如果有必要的话),直至计数器大于零,然后线程会减少这个计数。

对应的函数有:

  1. dispatch_semaphore_create:创建dispatch信号量
  2. dispatch_semaphore_signal:信号通知
  3. dispatch_semaphore_wait:信号等待

应用技巧

创建单例可以用来保证线程安全:

static Foo *sharedFoo;
+(Foo*)sharedFoo
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedFoo = [[Foo alloc] init];
    });
    
    return sharedFoo;
}

不过我平时更爱使用这种方式来创建单例:

static Foo *sharedFoo = nil;
+ (void)initialize
{
    if (self == [Foo class]) {
        sharedFoo = [[Foo alloc] init];
        [sharedFoo loadUser];
    }
}
+ (Foo*)sharedFoo
{
    return sharedFoo;
}

其他: 另一个喜欢的技巧写在GCD异步访问URL的宏

TODO

需补全知识点:

  • Dispatch I/O
  • Dispatch Barriers
  • Dispatch Data Objects

参考资料