《a series about Grand Central Di
本篇是a series about Grand Central Dispatch的学习记录。
原文共6篇,循序渐进地介绍了GCD的基本使用及相关规范。
1. Why GCD?
1.1 在GCD出现之前,解决数据竞争的方式
- 信号(Semaphore):只允许使用固定的有限资源。在资源可用之前,线程需要一直等待。
- 互斥锁(Mutual Exclusion):一次只允许一个线程执行。当前线程拥有锁时,其他线程等待。
- 条件变量(Condition Variable):在指定条件为真之前,线程等待。
1.2 GCD队列
分为串行队列、并发队列和主队列三种。
1.3 Blocks
Blocks可以捕获上下文中的变量(保存当时的值)。
1.4 你好,GCD
// 1.
dispatch_main();
// 2.
[[NSRunLoop currentRunLoop] run];
以上两者均可以启动主线程运行(在main函数中可以使队列不返回,程序不结束。)。但是建议用后者,因为NSRunLoop对象中可以支持NSTimer等资源执行,前者不可以。需要退出当前队列,在对应队列中执行exit(0)即可。
2. Using GCD Queues For Synchronization
传统情况下,数据竞争(多线程环境下更新同一份数据)需要使用排它锁进行数据更新,使用GCD可以通过串行队列直接解决。
3. GCD Concurrent Queues
- GCD的并发队列是代替多线程的一种更好的方式。
- GCD会自动管理在并发队列中运行任务的线程(创建、释放和重用等)。
- 使用GCD的栅栏配合并发队列,可以很容易创建读写锁(并发读取,同步写入)。
4. GCD Target Queues
- 所有我们自定义的队列都有一个目标队列。默认此目标队列即为默认优先级的全局并发队列。
- 当进队的block准备执行时,队列将会把此block重进使其进入到目标队列中,由目标队列进行block真正的执行。
- 只有全局并发队列和主队列二者才有真正执行block的能力。其他任何队列都是将此二者之一作为目标队列的。
4.1 使用目标队列规范执行时机
直接看栗子:
#import <Foundation/Foundation.h>
void makeCall(dispatch_queue_t queue, NSString *caller, NSArray *callees) {
// 获取随机接通对象
NSInteger targetIndex = arc4random() % callees.count;
NSString *callee = callees[targetIndex];
NSLog(@"%@ 正在呼叫 %@...", caller, callee);
sleep(1);
NSLog(@"%@ 与 %@ 呼叫完毕!", caller, callee);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(arc4random() % 1000 * NSEC_PER_SEC)), queue, ^{
makeCall(queue, caller, callees);
});
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray *house1Folks = @[@"小明", @"小华", @"小希"];
NSArray *house2Folks = @[@"大明", @"大华", @"大希"];
// 创建并发队列,用于执行block
dispatch_queue_t house1Queue = dispatch_queue_create("com.jiji.concurrent", DISPATCH_QUEUE_CONCURRENT);
// 创建串行队列,作为并发队列的目标队列
dispatch_queue_t targetQueue = dispatch_queue_create("com.jiji.targetQueue", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(house1Queue, targetQueue);
for (NSString *caller in house1Folks) {
dispatch_async(house1Queue, ^{
makeCall(house1Queue, caller, house2Folks);
});
}
// 使主队列不返回
[[NSRunLoop currentRunLoop] run];
// dispatch_main(); // Xcode都无法结束调试
}
return 0;
}
执行结果为:
2019-01-28 11:09:09.259488+0800 GCDTest[92057:9310107] 小明 正在呼叫 大明...
2019-01-28 11:09:10.260182+0800 GCDTest[92057:9310107] 小明 与 大明 呼叫完毕!
2019-01-28 11:09:10.260306+0800 GCDTest[92057:9310107] 小华 正在呼叫 大希...
2019-01-28 11:09:11.262624+0800 GCDTest[92057:9310107] 小华 与 大希 呼叫完毕!
2019-01-28 11:09:11.262751+0800 GCDTest[92057:9310107] 小希 正在呼叫 大希...
2019-01-28 11:09:12.264263+0800 GCDTest[92057:9310107] 小希 与 大希 呼叫完毕!
使用目标队列,在本例中,通过将串行队列设置为house1Queue的目标队列,将本来在并发队列中对于block的无序执行变为了串行执行。故dispatch_set_target_queue可以将原队列的优先级设置为与目标队列相同,在这里即变为了串行执行。而由于自定义的串行队列默认的目标为全局并发队列,故实际上block是在全局队列中重进进队并执行。
4.2 使指定串行队列作为多个队列的共同目标
这样设置,可以将子队列设置为同级(实质优先级相同,且执行block时都在目标队列中)。
4.3 现实当中的应用
- 设置多个目标队列目标为一个串行队列,可以让你在多线程环境下,执行任务时变得有序可控。也就是这里所说的“同一时刻只做一件事”:如数据库访问、硬盘读写或其他硬件资源的访问等。
- 当需要在一大堆不同类型的事件资源中进行有序处理,比如“同一时间只做一件事”时,这种模式也非常有用。如,处理定时器、网络事件或文件系统活动等。
- 当处理不同框架的不同事件时,在你不方便修改源代码时,使用此模式可能也能解决问题。
注意:
- 对于必须并发执行的操作,使用这种模式时会发生死锁。
- 一般来说,直接在串行队列中使用dispatch_async派发block即可代替本模式。**
5. Writing Thread-Safe Classes with GCD
线程安全,一般来说,在GCD中都需要使用串行队列或者在并发队列中的派发执行栅栏任务。故一定需要记住一点:线程安全是以牺牲性能为代价的,一定不要滥用。
使用GCD实现线程安全的类,需要在内部设置上述二者之一的队列。作者以串行队列为例,并以优雅的封装方式进行了阐述。直接贴完整代码:
// 头文件(公共API中没有任何需要调用者额外注意的事件)
@interface Warrior: NSObject
@property (nonatomic, strong) NSString *leftHandEquippedItem;
@property (nonatomic, strong) NSString *rightHandEquippedItem;
- (void)swapLeftAndRightHandEquippedItems;
- (NSString *)juggleNewItem:(NSString *)item; // return dropped item
@end
// 实现文件
@interface Warrior()
/** 串行队列,线程安全的基础 */
@property (nonatomic, strong) dispatch_queue_t memberQueue;
/** 线程安全的内部版本,该命名方式表明需要在队列中使用 */
@property (nonatomic, strong) NSString *memberQueueLeftHandEquippedItem;
/** 线程安全的内部版本,该命名方式表明需要在队列中使用 */
@property (nonatomic, strong) NSString *memberQueueRightHandEquippedItem;
@end
@implementation Warrior
- (id)init {
self = [super init];
if (self) {
// 由于队列对象非常“轻”,实例化时可放心创建
_memberQueue = dispatch_queue_create("Queue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
// private的setter(由于内部版本需要在队列中使用,故直接赋值即可)
- (void)setMemberQueueLeftHandEquippedItem:(NSString *)item {
NSLog(@"Left hand now holds %@", item);
_memberQueueLeftHandEquippedItem = item;
}
- (void)setMemberQueueRightHandEquippedItem:(NSString *)item {
NSLog(@"Right hand now holds %@", item);
_memberQueueRightHandEquippedItem = item;
}
// public的getter(外部API的实现是调用的内部版本,需要在队列中使用,保证线程安全)
- (NSString *)leftHandEquippedItem {
__block NSString *retval;
dispatch_sync(self.memberQueue, ^{
retval = self.memberQueueLeftHandEquippedItem;
});
return retval;
}
- (NSString *)rightHandEquippedItem {
__block NSString *retval;
dispatch_sync(self.memberQueue, ^{
retval = self.memberQueueRightHandEquippedItem;
});
return retval;
}
// public的setter(外部API的实现是调用的内部版本,需要在队列中使用,保证线程安全)
- (void)setLeftHandEquippedItem:(NSString *)item {
dispatch_sync(self.memberQueue, ^{
self.memberQueueLeftHandEquippedItem = item;
});
}
- (void)setRightHandEquippedItem:(NSString *)item {
dispatch_sync(self.memberQueue, ^{
self.memberQueueRightHandEquippedItem = item;
});
}
// private的method(由于内部版本需要在队列中使用,故直接赋值即可)
- (void)memberQueueSwapLeftAndRightHandEquippedItems {
NSString *oldLeftHandEquippedItem = self.memberQueueLeftHandEquippedItem;
self.memberQueueLeftHandEquippedItem = self.memberQueueRightHandEquippedItem;
self.memberQueueRightHandEquippedItem = oldLeftHandEquippedItem;
}
// public的method(外部API的实现是调用的内部版本,需要在队列中使用,保证线程安全)
- (void)swapLeftAndRightHandEquippedItems {
dispatch_sync(self.memberQueue, ^{
[self memberQueueSwapLeftAndRightHandEquippedItems];
});
}
- (NSString *)juggleNewItem:(NSString *)item {
__block NSString *retval;
// 这里再说一遍,由于在队列中执行,故block内直接使用内部版本的变量和方法
dispatch_sync(self.memberQueue, ^{
retval = self.memberQueueRightHandEquippedItem;
self.memberQueueRightHandEquippedItem = item;
// 这里调用外部方法会导致死锁
[self memberQueueSwapLeftAndRightHandEquippedItems];
});
return retval;
}
@end
以上便实现了一个“线程安全”类。其编码方式需要我们学习:
- 外部API(即头文件的代码)的声明与普通类无异,使用者无需关心内部实现,直接使用即可。
- 内部实现文件中,将对应需要进行线程安全处理的属性和方法等单独声明并实现私有的版本,且在命名上进行区分(如代码中的“memberQueue-”前缀):不仅增强了代码的可读性(根据命名即可知道是线程安全的版本);还可以在当类的属性和方法增多,代码量增大时,保证类结构的清晰。
- 除了init和dealloc等方法中,尽量不要直接使用实例变量ivar,保证代码的风格统一,可读性不变。
6. Keeping Things Straight with GCD
6.1 设计线程安全的类或库
-
设计线程安全的类或者库时,要把“线程安全”的细节设置到实现文件中,头文件或公有API中不要有相关体现(如线程和队列等)。
-
若整个库中的多个类都需要保证“线程安全”,使用同一个串行队列一般即可保证性能。如将此队列设置为私有的公共单例对象:
// Bank_Private.h
dispatch_queue_t memberQueue();
// Bank.m
#import "Bank_Private.h"
dispatch_queue_t memberQueue() {
static dispatch_queue_t queue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("member queue", DISPATCH_QUEUE_SERIAL);
});
return queue;
}
- 在设计“线程安全”的类或者库时,若要时刻保证调用正确,结构清晰,且不出现死锁或数据竞争的情况。需要遵循三条命名原则:
- 必须在队列内访问的变量或方法,其命名必须以该队列的名称做前缀。
- 向队列中派发的block,其内部只能访问那些以队列前缀命名的变量或方法。
- 在“前缀”命名的方法中,只能访问带有同样前缀的变量或方法。
6.2 使用单个队列的简易性
6.2.1 创建“读写锁”
“读写锁”,即“随意读取,写入保护”。在创建时,可以设置两个带有前缀的方法,如“memberQueue_”和“memberQueueMutating_”。前者只能读取变量的值或调用无修改功能的方法,后者可以对变量进行读写或调用任意方法(包括可修改数据的方法)。
- “memberQueue_”前缀的方法,内部可以随意使用dispatch_sync或是dispatch_async进行配置;
- “memberQueueMutating_”前缀的方法,内部必须使用dispatch_barrier_sync或是dispatch_barrier_async进行配置。
6.2.2 不要使用多个,嵌套的队列
- 自己的类的结构中,最多只需要一个串行队列即可。
- 一般来说,如果在一个类中,在需要使用多个串行队列来实现一个功能时,出于性能考虑,可以直接单独使用一个并发队列来进行替换。