如果使用GCD,完全由系统管理线程,我们不需要编写线程代码。只需定义想要执行的任务,然后添加到适当的分派队列(dispatch queue)即向队列中添加代码块。GCD会负责创建线程和调度你的任务,系统直接提供线程管理。
和操作队列不同的是,块添加到分派队列后就无法取消了。分派队列是严格的FIFO结构,所以无法在队列中使用优先级或调整块的次序。如果需要这些特性,应该使用NSOperationQueue。不过,分派队列可可以完成一些操作无法完成的事情。
1.分派队列
GCD有5个不同的队列:运行在主线程中的main queue,3个不同优先级的后台队列,以及一个优先级更低的后台队列(用于I/O)。另外,开发者可以创建自定义队列:串行或者并行队列。自定义队列非常强大,在自定义队列中被调度的所有block最终都将被放入到系统的全局队列中和线程池中。
2.创建和管理dispatch queue
1).concurrent(并发) dispatch queue
并发dispatch queue可以同时并行地执行多个任务,不过并发queue仍然按先进先出的顺序来启动任务。并发queue会在之前的任务完成之前就出列下一个任务并开始执行。
系统给每个应用提供三个并发dispatch queue:
DISPATCH_QUEUE_PRIORITY_DEFAULT,
DISPATCH_QUEUE_PRIORITY_HIGH,
DISPATCH_QUEUE_PRIORITY_LOW
整个应用内全局共享,三个queue的区别是优先级。你不需要显式地创建这些queue,一般使用dispatch_get_global_queue函数来获取这三个queue:
// 获取默认优先级的全局并发dispatch queue
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
2).serial(串行) dispatch queue
应用的任务需要按特定顺序执行时,就需要使用串行Dispatch Queue,串行queue每次只能执行一个任务。你可以使用串行queue来替代锁,保护共享资源或可变的数据结构。和锁不一样的是,串行queue确保任务按可预测的顺序执行。而且只要你异步地提交任务到串行queue,就永远不会产生死锁。
和并发queue不同,我们必须显式地创建和管理所有你使用的串行queue,应用可以创建任意数量的串行queue,但不要为了同时执行更多任务而创建更多的串行queue。如果你需要并发地执行大量任务,应该把任务提交到全局并发queue
利用dispatch_queue_create函数创建串行queue,两个参数分别是queue名和一组queue属性
// 创建一个串行queue
dispatch_queue_t queue = dispatch_queue_create("queueName", NULL);
当你在网上搜索例子时,你会经常看人们传递0或者NULL给dispatch_queue_create的第二个参数。这是一个创建串行队列的过时方式;明确你的参数总是更好。
3).运行时获得公共Queue
a.使用dispatch_get_current_queue函数作为调试用途,或者测试当前queue的标识。
b.使用dispatch_get_main_queue函数获得应用主线程关联的串行dispatch queue。
c.使用dispatch_get_global_queue来获得共享的并发queue。
3.添加任务到queue
要执行一个任务,你需要将它添加到一个适当的dispatch queue,你可以单个或按组来添加,也可以同步或异步地执行一个任务。一旦进入到queue,queue会负责尽快地执行你的任务。一般可以用一个block来封装任务内容。
1.添加单个任务到queue
a.dispatch_async
异步地调度任务,绝大多数情况下,我们都会异步添加任务,因为添加任务到Queue中时,无法确定这些代码什么时候能够执行。因此异步地添加block或函数,可以让你立即调度这些代码的执行,然后调用线程可以继续去做其它事情。特别是应用主线程一定要异步地dispatch任务,这样才能及时地响应用户事件。
何时以及何处使用dispatch_async:
- 自定义串行队列:当你想串行执行后台任务并追踪它时就是一个好选择。这消除了资源争用,因为你知道一次只有一个任务在执行。注意若你需要来自某个方法的数据,你必须内联另一个 Block 来找回它或考虑使用 dispatch_sync。
- 主队列(串行):这是在一个并发队列上完成任务后更新 UI 的共同选择。要这样做,你将在一个 Block 内部编写另一个 Block 。以及,如果你在主队列调用 dispatch_async 到主队列,你能确保这个新任务将在当前方法完成后的某个时间执行。
- 并发队列:这是在后台执行非UI工作的共同选择。
b.dispatch_sync
少数时候你可能希望同步地调度任务,以避免竞争条件或其它同步错误。使用dispatch_sync和dispatch_sync_f函数同步地添加任务到Queue,这两个函数会阻塞当前调用线程,直到相应任务完成执行。注意:绝对不要在任务中调用 dispatch_sync函数,并同步调度新任务到当前正在执行的queue。对于串行queue这一点特别重要,因为这样做肯定会导致死锁;而并发queue也应该避免这样做。
何时以及何处使用dispatch_sync :
- 自定义串行队列:在这个状况下要非常小心!如果你正运行在一个队列并调用 dispatch_sync 放在同一个队列,那你就百分百地创建了一个死锁。
- 主队列(串行):同上面的理由一样,必须非常小心!这个状况同样有潜在的导致死锁的情况。
- 并发队列:这才是做同步工作的好选择,不论是通过调度障碍,或者需要等待一个任务完成才能执行进一步处理的情况。
我们举例使用GCD来创建一个并发queue异步加载
- (void)showImage{
dispatch_queue_t concurrentQueue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
__block UIImage *image = nil;
dispatch_sync(concurrentQueue, ^{//同步
//download image
NSLog(@"showImage thread is %@",[NSThread currentThread]);
NSError *downError = nil;
NSData *imageData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:kURL]]
returningResponse:nil
error:&downError];
if (downError == nil&&imageData !=nil) {
image = [UIImage imageWithData:imageData];
}else if (downError != nil){
NSLog(@"error happen:%@",downError);
}else{
NSLog(@"no data can get from the url");
}
});
dispatch_async(concurrentQueue, ^{//异步
//show image
if (image != nil){
[self.imageView setImage:image];
NSLog(@"image loading");
}else{
NSLog(@"image isn't download ,nothing to display");
}
});
});
}
2.在主线程中执行任务
GCD提供一个特殊的dispatchqueue,可以在应用的主线程中执行任务。调用dispatch_get_main_queue函数获得应用主线程的dispatch queue,添加到这个queue的任务由主线程串行化执行。
- (void)showImage{
//...省略
// 回到主线程显示图片
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
});
}
4.暂停和继续queue
我们可以使用dispatch_suspend函数暂停一个queue以阻止它执行block对象;使用dispatch_resume函数继续dispatch queue。调用dispatch_suspend会增加queue的引用计数,调用dispatch_resume则减少queue的引用计数。当引用计数大于0时,queue就保持挂起状态。因此你必须对应地调用suspend和resume函数。挂起和继续是异步的,而且只在执行block之间(比如在执行一个新的block之前或之后)生效。挂起一个queue不会导致正在执行的block停止。
5.Dispatch Group
我们可以使用dispatch_group_async函数将多个任务关联到一个Dispatch Group和相应的queue中,group会并发地同时执行这些任务。而且Dispatch Group可以用来阻塞一个线程, 直到group关联的所有的任务完成执行。有时候你必须等待任务完成的结果,然后才能继续后面的处理。
-(void)dispatchGroup1{
NSLog(@"group1");
}
-(void)dispatchGroup2{
NSLog(@"group2");
}
-(void)dispatchGroup3{
NSLog(@"group3");
}
-(void)dispatchGroup{
dispatch_group_t taskGroup = dispatch_group_create();
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_group_async(taskGroup, mainQueue, ^{
[self dispatchGroup1];
});
dispatch_group_async(taskGroup, mainQueue, ^{
[self dispatchGroup2];
});
dispatch_group_async(taskGroup, mainQueue, ^{
[self dispatchGroup3];
});
//dispatch_group_notify 以异步的方式工作。当 Dispatch Group中没有任何任务时会开始执行
dispatch_group_notify(taskGroup, mainQueue, ^{
[[[UIAlertView alloc]initWithTitle:@"Finish" message:@"all task are finished" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]show];
});
}
6.dispatch_after
我们可以使用dispatch_after延后任务的执行:
- (void)dispatchAfterSeconds{
NSLog(@"current thread is %@",[NSThread currentThread]);
double delayInSeconds = 4.0;
//指定一个距离现在3秒的时间delayInNanoSeconds
dispatch_time_t delayInNanoSeconds = dispatch_time(DISPATCH_TIME_NOW, (int64_t)delayInSeconds*NSEC_PER_SEC);
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_after(delayInNanoSeconds, concurrentQueue, ^{
NSLog(@"I'm showing");
});
}
dispatch_after就像一个延迟版的dispatch_async 。你依然不能控制实际的执行时间,且一旦 dispatch_after 返回也就不能再取消它。一般来讲,我们会在主队列上使用它。
7.dispatch_once
我们看下面的例子:
-(void)PerformingTaskOnlyOnce{
void(^executeOnlyOnce)(void)=^{
static NSUInteger numberOfEntries = 0;
numberOfEntries++;
NSLog(@"Executed %lu time(s)",(unsigned long)numberOfEntries);
};
static dispatch_once_t onceToken;
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_once(&onceToken, ^{
dispatch_async(concurrentQueue, executeOnlyOnce);
});
dispatch_once(&onceToken, ^{
dispatch_async(concurrentQueue, executeOnlyOnce);
});
}
打印结果
Executed 1 time(s)
从打印结果可以得知,尽管我们调用了两次dispatch_once,但实际上只执行了一次。通常我们使用dispatch_once来写单例:
我们建立一个PhotoManager类,PhotoManager.m如下:
@interface PhotoManager ()
@property (nonatomic, strong) NSMutableArray *photosArray;
@property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue;
@end
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
});
return sharedPhotoManager;
}
说起单例,我们必须来讲一个概念:线程安全:
线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是 NSDictionary 。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary 就不是线程安全的,应该保证一次只能有一个线程访问它。
如果我们上面的代码是这样写的:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
if (!sharedPhotoManager) {
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
}
return sharedPhotoManager;
}
这时if条件分支不是线程安全的;如果你多次调用这个方法,有一个可能性是在某个线程(就叫它线程A)上进入 if 语句块并可能在 sharedPhotoManager 被分配内存前发生一个上下文切换。然后另一个线程(线程B)可能进入 if ,分配单例实例的内存,然后退出。
当系统上下文切换回线程A,你会分配另外一个单例实例的内存,然后退出。在那个时间点,你有了两个单例的实例——很明显这不是你想要的,所以使用dispatch_once() 可以以线程安全的方式执行且仅执行其代码块一次。试图访问临界区(即传递给 dispatch_once 的代码)的不同的线程会在临界区已有一个线程的情况下被阻塞,直到临界区完成为止。不过这只是让访问共享实例线程安全。它绝对没有让类本身线程安全。类中可能还有其它竞态条件,例如任何操纵内部数据的情况。
8.dispatch barriers
线程安全实例不是处理单例时的唯一问题。如果单例属性表示一个可变对象,那么你就需要考虑是否那个对象自身线程安全。例如上面的sharedPhotoManager->_photosArray。
虽然许多线程可以同时读取 NSMutableArray 的一个实例而不会产生问题,但当一个线程正在读取时让另外一个线程修改数组就是不安全的。你的单例在目前的状况下不能预防这种情况的发生。
假设我们有一个PhotoManager的类,PhotoManager.m中的addPhoto如下:
@interface PhotoManager ()
@property (nonatomic, strong) NSMutableArray *photosArray;
@end
- (void)addPhoto:(Photo *)photo
{
if (photo) {
[_photosArray addObject:photo];
}
}
这是一个写方法,它修改一个私有可变数组对象。
现在看看photos :
- (NSArray *)photos
{
return [NSArray arrayWithArray:_photosArray];
}
这是所谓的读方法,它读取可变数组。它为调用者生成一个不可变的拷贝,防止调用者不当地改变数组,但这不能提供任何保护来对抗当一个线程调用读方法 photos 的同时另一个线程调用写方法addPhoto: 。
这就是软件开发中经典的读写问题。GCD通过用dispatch barriers创建一个读写锁提供了一个优雅的解决方案。
Dispatch barriers是一组函数,在并发队列上工作时扮演一个串行式的瓶颈。使用 GCD barrier API确保提交的Block在那个特定时间上是指定队列上唯一被执行的条目。这就意味着所有的先于调度障碍提交到队列的条目必能在这个Block执行前完成。
当这个Block的时机到达,调度障碍执行这个Block并确保在那个时间里队列不会执行任何其它Block。一旦完成,队列就返回到它默认的实现状态。 GCD 提供了同步和异步两种障碍函数。
下图显示了障碍函数对多个异步队列的影响:
正常部分的操作就如同一个正常的并发队列。但当障碍执行时,它本质上就如同一个串行队列。也就是,障碍是唯一在执行的事物。在障碍完成后,队列回到一个正常并发队列的样子。我们一般在自定义并发队列使用Dispatch barriers
将上面的例子中的写方法修改如下:
@interface PhotoManager ()
@property (nonatomic, strong) NSMutableArray *photosArray;
@property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue;
@end
- (void)addPhoto:(Photo *)photo
{
if (photo) {
dispatch_barrier_async(self.concurrentPhotoQueue, ^{
[_photosArray addObject:photo];
});
}
}
你还需要实现photos读方法。
在写入方法打扰的情况下,要确保线程安全,你需要在 concurrentPhotoQueue 队列上执行读操作。对于读操作,dispatch_sync是一个好的选择。
dispatch_sync() 同步地提交工作并在返回前等待它完成。使用dispatch_sync跟踪你的调度障碍工作,或者当你需要等待操作完成后才能使用Block处理过的数据。如果你使用第二种情况做事,你将不时看到一个__block变量写在dispatch_sync范围之外以便返回时在dispatch_sync使用处理过的对象。
但你需要很小心。想像如果你调用dispatch_sync并放在你已运行着的当前队列。这会导致死锁,因为调用会一直等待直到Block完成,但Block不能完成(它甚至不会开始!),直到当前已经存在的任务完成,而当前任务无法完成!这将迫使你自觉于你正从哪个队列调用——以及你正在传递进入哪个队列。
- (NSArray *)photos
{
__block NSArray *array; // 1
dispatch_sync(self.concurrentPhotoQueue, ^{ // 2
array = [NSArray arrayWithArray:_photosArray]; // 3
});
return array;
}
最后在单例sharedManager中实例化self.concurrentPhotoQueue:
+ (instancetype)sharedManager
{
static PhotoManager *sharedPhotoManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedPhotoManager = [[PhotoManager alloc] init];
sharedPhotoManager->_photosArray = [NSMutableArray array];
sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue",
DISPATCH_QUEUE_CONCURRENT);
});
return sharedPhotoManager;
}
现在PhotoManager单例现在是线程安全的了。