多线程编程是一项非常重要的技术,目前在iOS开发中比较流行的多线程方案是GCD和NSOperationQueue,本文将详细介绍如何使用GCD进行多线程编程。根据苹果的文档,GCD(Grand Central Dispatch)是一项提供了管理并发和异步执行任务的技术,开发者只需要将想要执行的任务追加到适当的dispatch queue中,GCD会为此生成必要的线程来执行任务。GCD使用非常简洁的语法实现了复杂的多线程编程方案,接下来我们深入认识它。
dispatch queue
GCD使用队列来实现多个任务的执行,队列分为两种,serial dispatch queue和concurrent dispatch queue,前者指队列中的任务会使用单一的线程逐个执行,而后者指队列中的任务会使用多个线程并行执行。我们可以自行创建这两种队列,方法如下:
dispatch_queue_t serialQueue = dispatch_queue_create(@"cn.test.serial.queue", NULL);
dispatch_queue_t concurrentQueue = dispatch_queue_create(@"cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_create函数用于创建一个队列,方法的第一个参数为队列的标签(可为空,但并不推荐这样做),第二个参数指定队列的类型,当指定为NULL时即指serial dispatch queue,指定为DISPATCH_QUEUE_CONCURRENT时为concurrent dispatch queue,需要注意的是iOS6之后,GCD已经纳入了ARC的管理范围,所以不再需要手动调用相关retain/release方法。
事实上,苹果为我们提供了默认的dispatch queue,供我们使用,因此自行创建dispatch queue并不总是必要的。默认的队列有两种main dispatch queue和global dispatch queue,前者队列中的任务会在主线程中去执行,主线程只有一个,所以它是serial dispatch queue,而后者则属于concurrent dispatch queue,队列中的任务会并发执行,此队列有四种优先级可供选择,分别为DISPATCH_QUEUE_PRIORITY_HIGH,DISPATCH_QUEUE_PRIORITY_DEFAULT,DISPATCH_QUEUE_PRIORITY_LOW,DISPATCH_QUEUE_PRIORITY_BACKGROUND ,使用默认队列的方式是这样的:
//获取main dispatch queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//获取global dispatch queue
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_get_global_queue函数的第一个参数指定队列优先级,第二个参数默认写0即可
dispatch_async
我们使用dispatch_async向某个队列追加一项任务,async表明该方式是异步的,不会阻塞当前线程,使用方式如下
dispatch_async(serialQueue, ^ {
NSLog(@"block");
});
该函数第一个参数指定向哪个队列追加任务,第二个参数是一个block,表示要执行的任务。
dispatch_set_target_queue
该函数有两个功能,第一,可以指定某个队列的优先级和目标队列优先级一致,例如
dispatch_queue_t concurrentQueue = dispatch_queue_create(@"cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_set_target_queue(concurrentQueue, globalQueue);
我们创建的队列都是默认优先级,上述代码表示指定concurrentQueue优先级和globalQueue优先级一致。
第二,可用于改变队列的执行层次。举个例子,现有A,B, C三个serial dispatch queue,如果指定这三个队列的target queue为D serial dispatch queue,那么原本会并行执行的三个队列在D上就会串行执行,示例如下:
dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue",NULL);
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
3 in
3 out
dispatch_after
该函数用于延时调用,例如当我们想要延时3秒执行某项任务时:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"test");
});
其中第一个参数是时间,类型为dispatch_time_t,上例中直接使用dispatch_time函数生成,表示从现在开始3秒之后的时间,第二个参数是要在哪个队列中执行,上例为main dispatch queue,第三个参数为要执行的block,该函数实际上是在3秒之后向队列追加了一个block任务。
dispatch_group
当我们有多个线程在同时执行任务,我们希望在所有线程中的任务执行完毕后做某项处理,那么就可以将前面的多个线程加入到一个group中,group会监控线程中的任务是否执行完毕,执行完毕可以发送通知,之后我们就可以做某项处理。示例如下:
dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("cn.test.serial.queue2", NULL);
dispatch_queue_t serialQueue3 = dispatch_queue_create("cn.test.serial.queue3", NULL);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, serialQueue1, ^ {
[NSThread sleepForTimeInterval:2];
NSLog(@"test");
});
dispatch_group_async(group, serialQueue2, ^ {
[NSThread sleepForTimeInterval:3];
NSLog(@"test2");
});
dispatch_group_async(group, serialQueue3, ^ {
[NSThread sleepForTimeInterval:1];
NSLog(@"test3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^ {
NSLog(@"finished");
});
//代码执行结果
test3
test
test2
finished
dispatch_group_notify函数会在group中所有线程中的任务执行完毕后被调用,因此上例不论前面线程执行谁先谁后,最后执行的一定是主线程中的block。例如有时候我们会通过多线程的方式去下载一些资源,数据等,然后所有下载完毕后在主线程中进行UI更新,就可以这样用。
除了使用dispatch_group_notify来等待group中所有任务执行完毕外,还可以通过dispatch_group_wait函数来实现同样效果
long ret = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
if (ret == 0) {
NSLog(@"finished");
}else
{
NSLog(@"unfinished");
}
需要说明的是该函数第二个参数是指等待时间,wait函数会使当前线程停止,等待直到:
- 在等待时间内,group中所有任务执行完毕,返回0
- 在等待时间内,group中任务没有执行完毕,超时返回非0
如上例所示,当设置时间为DISPATCH_TIME_FOREVER意味着永久等待,直到任务执行完毕。
dispatch_barrier_async
当我们需要多个线程对某个属性进行频繁的读写操作时,如果不对线程加以控制,很容易造成读写混乱,因此我们可能希望:当进行读操作时,可以多个线程同时读取以保证高效率,而进行写操作时,不可以有任何线程进行读操作,写入完毕,线程又可以并发读取属性值,这种情况barrier技术正好可以方便的解决问题,示例如下
dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^ { /*读取*/ });
dispatch_async(concurrentQueue, ^ { /*读取*/ });
dispatch_async(concurrentQueue, ^ { /*读取*/ });
dispatch_barrier_async(concurrentQueue, ^ { /*写入*/});
dispatch_async(concurrentQueue, ^ { /*读取*/ });
dispatch_async(concurrentQueue, ^ { /*读取*/ });
dispatch_sync
有dispatch_async,当然也就有dispatch_sync,该函数表示同步执行,即当前线程会等待sync中的任务执行完毕才继续往下执行,例如
dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
dispatch_sync(serialQueue1, ^ {
NSLog(@"test");
});
在主线程中执行上述代码,当执行到dispatch_sync时,线程会进行等待,直到追加到serialQueue1中的block执行完毕,主线程才继续往下执行。这种同步调用如果不加注意,就容易造成线程死锁问题,例如在主线程中执行下述代码就会造成死锁问题,主线程停止,等待lock执行完毕,而追加到主线程的block因为主线程停止而永远不会被执行,于是产生死锁。
dispatch_sync(dispatch_get_main_queue(), ^ {
NSLog(@"test");
});
dispatch_apply
该函数与dispatch_sync有些关联,是指重复向某个队列追加block并等待全部block执行完毕后返回。例如当我们想遍历某个数组,对所有元素做些操作,可能会使用循环的方式,当数组非常大的时候,我们可能希望使用多线程去遍历数组所有元素来提高效率,那么就可以使用dispatch_apply,示例如下
NSArray *array = /*数组赋值*/;
dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply([array count], concurrentQueue, ^(size_t index) {
NSLog(@"array item: %@", [array objectAtIndex:index]);
});
dispatch_apply的第一个参数指重复追加的次数,第二个为执行的队列,第三个为要执行的block,和前面不同的是,block里有一个参数,这个参数用来标识追加到队列中的每个block。上述代码中所有block在执行完毕后才能执行后续代码。
dispatch_suspend/dispatch_resume
这一对函数非常简单,dispatch_suspend用于挂起队列,之后队列中所有任务都会暂停执行,dispatch_resume使队列从暂停状态恢复为继续执行状态。
dispatch semaphore
dispatch_semaphore可以实现更加精细化地对线程进行管理。semaphore是拥有计数的信号量,我们可以通过semaphore对多线程访问共享资源时进行精细化的排他控制,也可以通过semaphore实现并发线程数量的控制。
为了便于理解,我们举一个停车的例子,假设停车场只剩下两个车位,这时候同时来了三辆车,那么势必只能开入两辆车,另外一辆需要先一旁等待,直到有车离开停车位,等待的车才能开入,我们用信号量来实现这段逻辑:
dispatch_semaphore_t sema = dispatch_semaphore_create(2);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^ {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"into 1");
[NSThread sleepForTimeInterval:1];
NSLog(@"out 1");
dispatch_semaphore_signal(sema);
});
dispatch_async(globalQueue, ^ {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"into 2");
[NSThread sleepForTimeInterval:1];
NSLog(@"out 2");
dispatch_semaphore_signal(sema);
});
dispatch_async(globalQueue, ^ {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
NSLog(@"into 3");
[NSThread sleepForTimeInterval:1];
NSLog(@"out 3");
dispatch_semaphore_signal(sema);
});
//代码执行结果
2017-06-05 12:32:57.337284+0800 Demo[16270:3721871] into 1
2017-06-05 12:32:57.337416+0800 Demo[16270:3721882] into 2
2017-06-05 12:32:58.341023+0800 Demo[16270:3721882] out 2
2017-06-05 12:32:58.341199+0800 Demo[16270:3721871] out 1
2017-06-05 12:32:58.341402+0800 Demo[16270:3721870] into 3
2017-06-05 12:32:59.345365+0800 Demo[16270:3721870] out 3
操作信号量的主要有三个函数:
dispatch_semaphore_create用来创建信号量,参数表示初始的信号总量。
dispatch_semaphore_wait表示等待信号量,每执行一次信号量减1,第二个参数是指等待时间,DISPATCH_TIME_FOREVER意味着永久等待,该函数会使当前线程处于等待状态,直到以下两种情况才会返回:
- 在等待时间内,信号量大于等于1,这时wait函数返回0,信号量减1。
- 超出了等待时间,信号量依旧不满足大于等于1,wait函数因超时返回非0。
dispatch_semaphore_signal表示发送信号量,每执行一次信号量加1
我们也可以使用信号量对并发线程数量进行控制,示例如下:
dispatch_semaphore_t sema = dispatch_semaphore_create(10);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 100; i++) {
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_async(globalQueue, ^ {
NSLog(@"test: %d", i);
[NSThread sleepForTimeInterval:1];
dispatch_semaphore_signal(sema);
});
}
该例可以这样理解,信号量初始为10,for循环在创建了10个线程后,信号量减为0,于是for循环就进行等待,直到某一个线程结束,增加了一个新的信号量,才能继续执行,这样便控制了并发数量不超过10个。
dispatch_once
该函数比较简单,常常用于单例的生成,它表示其代码块在程序中只会执行一次,用法如下:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"some init...");
});
虽然使用@synchronized也能保证单例的线程安全,但dispatch_once性能要远高于@synchronized,因此单例创建推荐使用dispatch_once
dispatch I/O
dispatch I/O是指使用多线程进行文件读取的技术。主要思想是当文件比较大时,我们可以将文件分段,然后使用多线程进行读取,再合并,这种方式可以大大提高文件读取速度。
dispatch source
dispatch source是指在内核发生各种事件时,开发者可以执行自定义处理的技术。内核中事件的类型有多种,最常见的一种是定时器事件,使用示例如下:
dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, serialQueue1);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"test");
});
dispatch_resume(timer);
上例创建了一个定时器,每两秒执行一次block块。dispatch_source_set_event_handler函数设置了每次定时器事件触发时执行的处理,通过dispatch_resume启动该定时器。
当需要取消该定时器时,可以通过dispatch_source_cancel取消,另外还可以指定取消时要执行的处理。
dispatch_source_cancel(timer);
dispatch_source_set_cancel_handler(timer, ^ {
NSLog(@"cancel");
});
最后
GCD是基于内核级别的实现,在性能上是非常优异的,而且它语法简洁,是一套非常好的多线程编程方案。