1.为什么用GCD
GCD 是苹果公司为多核的并行运算提出的解决方案。
- 它会自动利用更多的CPU内核(比如双核、四核),
- 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
2.任务(同步异步) & 队列(串行并发)
任务 :就是你在线程中执行的那段代码。执行任务有两种方式:『同步执行』 和 『异步执行』 。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
- 同步 在当前线程中执行任务,任务未执行完时,会阻塞线程,不会开辟线程。使用方法如下
objc
dispatch_sync(queue, ^{
// do something
});
- 异步 可以在新的线程中执行任务,具备开启新线程的能力。使用方法如下
objc
dispatch_async(queue, ^{
// do something
});
GCD 中有两种队列:『串行队列』 和 『并发队列』,两者都符合 FIFO(先进先出)的原则。
- 串行 只开启一个线程,一个任务执行完毕后,再执行下一个任务。
- 并发 可以开启多个线程,并且同时执行任务。
objc
/*
第一个参数表示队列的唯一标识符,用于 DEBUG,可为空。
队列的名称推荐使用应用程序 ID 这种逆序全程域名。
第二个参数用来识别是串行队列还是并发队列。
`DISPATCH_QUEUE_SERIAL` 表示串行队列,`DISPATCH_QUEUE_CONCURRENT` 表示并发队列。
*/
dispatch_queue_t queue = dispatch_queue_create("com.test.company", DISPATCH_QUEUE_SERIAL);
注意:并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。
3.组合使用
同步串行
任务一个接一个执行,不开辟线程
objc
/**
串行队列,同步执行
*/
- (void)createSerialQueueWithSync {
dispatch_queue_t serialQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
NSLog(@"A==%@", [NSThread currentThread]);
dispatch_sync(serialQueue, ^{
NSLog(@"B==%@", [NSThread currentThread]);
});
dispatch_sync(serialQueue, ^{
NSLog(@"C==%@", [NSThread currentThread]);
});
NSLog(@"D==%@", [NSThread currentThread]);
}
结果如下
同步并发
任务一个接一个执行,不开辟线程
objc
/**
并行队列,同步执行
*/
- (void)createConcurrentQueueWithSync {
dispatch_queue_t conCurrentQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_CONCURRENT);
// dispatch_queue_t conCurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//这个是全局并行队列,一般并行任务都会加到这里面去
dispatch_sync(conCurrentQueue, ^{
NSLog(@"A==%@", [NSThread currentThread]);
});
dispatch_sync(conCurrentQueue, ^{
NSLog(@"B==%@", [NSThread currentThread]);
});
dispatch_sync(conCurrentQueue, ^{
NSLog(@"C==%@", [NSThread currentThread]);
});
NSLog(@"D==%@", [NSThread currentThread]);
}
打印结果如下
异步串行
任务一个接一个执行,会开辟线程。
objc
/**
串行队列,异步执行
*/
- (void)createSerialQueueWithAsync {
dispatch_queue_t serialQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
NSLog(@"A==%@", [NSThread currentThread]);
});
dispatch_async(serialQueue, ^{
NSLog(@"B==%@", [NSThread currentThread]);
});
dispatch_async(serialQueue, ^{
NSLog(@"C==%@", [NSThread currentThread]);
});
NSLog(@"D==%@", [NSThread currentThread]);
}
打印结果如下
异步并发
任务乱序执行,开辟线程。
objectivec
/**
并行队列,异步执行
*/
- (void)createConcurrentQueueWithAsync {
dispatch_queue_t conCurrentQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(conCurrentQueue, ^{
NSLog(@"A==%@", [NSThread currentThread]);
});
dispatch_async(conCurrentQueue, ^{
NSLog(@"B==%@", [NSThread currentThread]);
});
dispatch_async(conCurrentQueue, ^{
NSLog(@"C==%@", [NSThread currentThread]);
});
NSLog(@"D==%@", [NSThread currentThread]);
}
打印结果如下
4.GCD其他用法
dispatch_barrier
我们有时需要执行两组操作,且要求第一组(任务1、2、3)操作执行完之后,才能开始执行第二组(任务4、5、6)操作。这时就需要用到栅栏函数,栅栏函数的作用如下图所示
从图中我们可以看出
- 对于串行队列来说,任务是一个接一个执行的,所以对串行队列使用栅栏函数意义不大
- 对于并发队列来说,任务是乱序执行的,所以使用栅栏函数可以很好的控制队列内任务执行的顺序
使用
举例如下
objc
- (void)test {
dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"开始------%@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(2);
NSLog(@"延迟2s的任务1------%@", [NSThread currentThread]);
});
NSLog(@"第一次结束------%@", [NSThread currentThread]);
// dispatch_barrier_async(queue, ^{
// NSLog(@"----------栅栏任务----------%@", [NSThread currentThread]);
// });
// NSLog(@"栅栏结束------%@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"延迟1s的任务2------%@", [NSThread currentThread]);
});
NSLog(@"第二次结束------%@", [NSThread currentThread]);
}
不使用栅栏函数
ini
开始------<NSThread: 0x600002384f00>{number = 1, name = main}
第一次结束------<NSThread: 0x600002384f00>{number = 1, name = main}
第二次结束------<NSThread: 0x600002384f00>{number = 1, name = main}
延迟1s的任务2------<NSThread: 0x6000023ec300>{number = 5, name = (null)}
延迟2s的任务1------<NSThread: 0x60000238c180>{number = 7, name = (null)}
使用栅栏函数
ini
开始------<NSThread: 0x600000820bc0>{number = 1, name = main}
第一次结束------<NSThread: 0x600000820bc0>{number = 1, name = main}
栅栏结束------<NSThread: 0x600000820bc0>{number = 1, name = main}
第二次结束------<NSThread: 0x600000820bc0>{number = 1, name = main}
延迟2s的任务1------<NSThread: 0x600000863c80>{number = 4, name = (null)}
----------栅栏任务----------<NSThread: 0x600000863c80>{number = 4, name = (null)}
延迟1s的任务2------<NSThread: 0x600000863c80>{number = 4, name = (null)}
总结一句话就是: 先执行栅栏前任务
,再执行栅栏任务
,最后执行栅栏后任务
。
问题
1、dispatch_barrier_async
和dispatch_barrier_sync
有什么区别,该用那个?
两者作用相同,都是先执行栅栏前任务
,再执行栅栏任务
,最后执行栅栏后任务
。但是dispatch_barrier_sync
会阻塞线程,影响后面的任务执行(尽量少用)。
比如将上例的dispatch_barrier_async
改成dispatch_barrier_sync
,结果如下
ini
开始------<NSThread: 0x600001040d40>{number = 1, name = main}
第一次结束------<NSThread: 0x600001040d40>{number = 1, name = main}
延迟2s的任务1------<NSThread: 0x60000100ce40>{number = 6, name = (null)}
----------栅栏任务----------<NSThread: 0x600001040d40>{number = 1, name = main}
栅栏结束------<NSThread: 0x600001040d40>{number = 1, name = main}
第二次结束------<NSThread: 0x600001040d40>{number = 1, name = main}
延迟1s的任务2------<NSThread: 0x60000100ce40>{number = 6, name = (null)}
注意:栅栏函数必须是自定义的并发队列才有效,且必须是同一队列中的线程才有效。
读写锁
读写锁 在读多写少的场景效率很高,它的定义是:同一时间有多个读者,同一时间只能有一个写者,读者和写者不能同时存在。
工作原理:读写锁 实际是一种特殊的自旋锁 ,如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里, 直到没有任何写者或读者。
写的那个时间段,不能有任何读者+其他写者,因此根据栅栏函数原理,我们可以把读写操作理解成下图
objc
- (id)readDataForKey:(NSString*)key {
__block id value;
dispatch_sync(_concurrentQueue, ^{
value = [self valueForKey:key];
});
return value;
}
- (void)writeData:(id)data forKey:(NSString*)key {
dispatch_barrier_async(_concurrentQueue, ^{
[self setValue:data forKey:key];
});
}
为什么用dispatch_sync
完成读,用dispatch_barrier_async
完成写?
- 读:使用
dispatch_sync
,会使 value 获取到值后,再返回给读者- 若使用
dispatch_async
则会先返回空的result value
,再通过[self valueForKey:key]
方法获取到值
- 若使用
- 写:写的那个时间段,不能有任何读者+其他写者,
dispatch_barrier_async
满足等队列中前面的读写任务都执行完了再来执行当前任务,而不阻塞当前线程
dispatch_group
调度组有以下能力
- 将任务分组执行
dispatch_group_enter(group), dispatch_group_leave(group)
- 能监听任务组完成
dispatch_notify(group, queue, ^{});
- 并设置等待时间
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_group_async
objc
- (void)createGroupQueue {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{
NSLog(@"A---%@", [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"B---%@", [NSThread currentThread]);
}
});
dispatch_group_async(group, queue, ^{
NSLog(@"C---%@", [NSThread currentThread]);
});
dispatch_notify(group, queue, ^{
NSLog(@"队列组任务执行完毕");
});
}
dispatch_group_enter & dispatch_group_leave
objc
- (void)test2 {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"A---%@", [NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"B---%@", [NSThread currentThread]);
}
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"C---%@", [NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_notify(group, queue, ^{
NSLog(@"队列组任务执行完毕");
});
}
dispatch_group_wait
dispatch_group_wait
可以设置调度组的等待时间,这也意味着会阻塞当前线程,等待调度组完成任务,若在指定时间内完成任务,会返回 0;否则返回非 0。
语法:long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)
- group:需要等待的调度组
- timeout:等待的超时时间(即等多久)
- 设置为
DISPATCH_TIME_NOW
意味着不等待直接判定调度组是否执行完毕 - 设置为
DISPATCH_TIME_FOREVER
则会阻塞当前调度组,直到调度组执行完毕 - 设置具体超时时间
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC))
- 设置为
- 返回值:为
long
类型- 0 表示在指定时间内调度组完成了任务
- 非0 表示指定时间内调度组没有按时完成任务
举例如下
objc
- (void)test {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"请求一完成");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"请求二完成");
dispatch_group_leave(group);
});
// long timeout = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// 设置 1 秒超时,在这 1 秒期间会阻塞当前线程
long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
NSLog(@"timeout=%ld", timeout);
if (timeout == 0) {
NSLog(@"按时完成任务");
} else {
NSLog(@"超时");
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"通知,所有请求完成");
});
}
结果如下
ini
timeout=49 // 49 就是非 0,即超时返回错误
超时
请求一完成
请求二完成
通知,所有请求完成
dispatch_semaphore
信号量可用于控制 GCD 最大并发数,还可以当锁使用(信号量锁的效率也很高)。
dispatch_semaphore_create(intptr_t value);
:创建信号量dispatch_semaphore_wait(semaphore, dispatch_time_t timeout)
:等待信号量,信号量-1
。当信号量< 0
时会阻塞当前线程,根据传入的等待时间决定接下来的操作(如果永久等待 将等到信号(signal)
才执行下去)dispatch_semaphore_signal(semaphore);
:释放信号量,信号量+1
。当信号量>= 0
会执行wait
之后的代码
比如AFURLSessionManager.m
中的tasksForKeyPath:
方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks。
objc
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}
dispatch_once
我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once
方法。使用 dispatch_once
方法能保证某段代码在程序运行过程中只被执行 1 次,并且即使在多线程的环境下,dispatch_once
也可以保证线程安全。
objc
/**
* 一次性代码(只执行一次)dispatch_once
*/
- (void)once {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行 1 次的代码(这里面默认是线程安全的)
});
}
有时dispatch_once
可用作优化,类似程序运行期间要多次获取的值,可以只执行一次耗时操作,然后把值存起来供下次直接使用,比如下例中获取设备等级的代码
objc
+ (NSInteger)getDeviceLevel {
static dispatch_once_t deviceLevelOnce;
static NSInteger deviceLevel;
dispatch_once(&deviceLevelOnce, ^{
struct utsname systemInfo;
uname(&systemInfo);
NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSASCIIStringEncoding];
NSBundle *bundle = [NSBundle c4_localizedBundle];
NSString *path = [bundle pathForResource:@"Apple_mobile_device_types" ofType:@"plist"];
NSDictionary *deviceDict = [NSDictionary dictionaryWithContentsOfFile:path];
NSDictionary *dict = deviceDict[deviceModel];
NSString *level = dict[@"level"];
if (!level) {
deviceLevel = 4;
} else {
deviceLevel = level.integerValue;
}
});
return deviceLevel;
}
dispatch_apply
objc
//快速迭代方法
dispatch_apply(size_t iterations,
dispatch_queue_t DISPATCH_APPLY_QUEUE_ARG_NULLABILITY queue,
DISPATCH_NOESCAPE void (^block)(size_t iteration));
当 queue 是串行队列时,它的作用和 for 循环一样,所以一般用于并行队列。
使用方式如下
objc
- (void)testApply {
NSLog(@"dispatch_apply 开始执行");
dispatch_apply(6, dispatch_get_global_queue(0, 0), ^(size_t iteration) {
NSLog(@"%zd---%@", iteration, [NSThread currentThread]);
});
NSLog(@"dispatch_apply 结束执行");
}
结果如下
ini
dispatch_apply 开始执行
1---<NSThread: 0x600003340d40>{number = 9, name = (null)}
2---<NSThread: 0x600003334000>{number = 11, name = (null)}
3---<_NSMainThread: 0x600003328080>{number = 1, name = main}
4---<NSThread: 0x60000331a4c0>{number = 12, name = (null)}
0---<NSThread: 0x60000331a3c0>{number = 10, name = (null)}
5---<NSThread: 0x600003348f40>{number = 13, name = (null)}
dispatch_apply 结束执行
无论是在串行队列,还是并发队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait
方法。
dispatch_source
常用应用场景是 GCD Timer。
一般使用NSTimer
来处理定时逻辑,但NSTimer
是依赖 Runloop 的,而 Runloop 可以运行在不同的模式下。如果NSTimer
添加在一种模式下,当 Runloop 运行在其他模式下的时候,定时器就挂机了;又如果 Runloop 在阻塞状态,NSTimer
触发时间就会推迟到下一个 Runloop 周期。因此NSTimer
在计时上会有误差,并不是特别精确,而 GCD 定时器不依赖 Runloop ,计时精度要高很多
常用 API
dispatch_source_create
: 创建事件源dispatch_source_set_event_handler
: 设置数据源回调dispatch_source_merge_data
: 设置事件源数据dispatch_source_get_data
: 获取事件源数据dispatch_resume
: 继续dispatch_suspend
: 挂起dispatch_cancle
: 取消
创建 GCD Timer 例子,摘自 YYTimer
objc
@property (nonatomic, strong) dispatch_source_t timer;
//创建timer
_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//设置timer首次执行时间,间隔,精确度
dispatch_source_set_timer(_source, dispatch_time(DISPATCH_TIME_NOW, (start * NSEC_PER_SEC)), (interval * NSEC_PER_SEC), 0);
//设置timer事件回调
__weak typeof(self) _self = self;
dispatch_source_set_event_handler(_source, ^{
[_self fire];
});
//默认是挂起状态,需要手动激活
dispatch_resume(_source);