1. 什么是GCD?
GCD 是 libdispatch 的市场名称,而 libdispatch 作为 Apple 的一个库,为并发代码在多核硬件(跑 iOS 或 OS X )上执行提供有力支持。它具有以下优点:
GCD 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。
GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。
GCD 具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。
2. 并行与并发
简单来说,若说两个任务A和B并发执行,则表示任务A和任务B在同一时间段里被执行(当计算机CPU只有一个核心在运行时,二者是交替执行的,即切换上下文,CPU在两个线程之间高速切换,让人感觉这两个任务是“同时”进行的);若说任务A和B并行执行,则表示任务A和任务B在同时被执行(这要求计算机CPU具有多核运算能力,即这两个任务分别在不同的核心上同时运行);
一句话:并行要求并发,但并发并不能保证并行。
3. Dispatch Queues
Dispatch Queue是一个任务执行队列,可以让你异步或同步地执行多个Block或函数。Dispatch Queue是FIFO的,即先入队的任务总会先执行。目前有三种类型的Dispath Queue:
- 串行队列(Serial dispatch queue)
- 并行队列(Concurrent dispatch queue)
- 主队列(Main dispatch queue)
3.1 串行队列(Serial dispatch queue)
serial dispatch queue中的block按照先进先出(FIFO)的顺序去执行,实际上为单线程执行。即每次从queue中取出一个task进行处理;用户可以根据需要创建任意多的serial dispatch queue,serial dispatch queue彼此之间是并发的,创建一个串行队列如下:
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MySerialQueue", DISPATCH_QUEUE_SERIAL);
3.2 并行队列(Concurrent dispatch queue)
相对于Serial Dispatch Queue,Concurrent Dispatch Queue一次性并发执行一个或者多个task;和Serial Dispatch Queue不同,系统提供了四个global concurrent queue,使用dispatch_get_global_queue函数就可以获取这些global concurrent queue;
和Serial Dispatch Queue一样,用户也可以根据需要自己定义concurrent queue;创建concurrent dispatch queue也使用dispatch_queue_create方法,所不同的是需要指定其第二个参数为DISPATCH_QUEUE_CONCURRENT即可:
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
3.3 主队列(Main dispatch queue)
application的主要任务(譬如UI管理之类的)都在main dispatch queue中完成;根据文档的描述,main dispatch queue中的task都在一个thread中运行,即application’s main thread(thread 1)。
所以,如果想要更新UI,则必须在main dispatch queue中处理,获取main dispatch queue也很容易,调用dispatch_get_main_queue()函数即可。
4. dispatch_sync和dispatch_async 同步和异步
dispatch_sync 派发的block的执行线程和 dispatch_sync 上下文线程是同一个线程;
dispatch_async 派发的block的执行线程和 dispatch_async 上下文线程不是同一个线程,即主队列 下异步任务还是在主队列下执行;
对于serial dispatch queue中的tasks,无论是同步派发还是异步派发,其执行顺序都遵循FIFO;
为了方便地使用 GCD,苹果提供了一些方法方便我们将 block 放在主线程 或 后台线程执行,或者延后执行。使用的例子如下:
// 后台执行:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// something
});
// 主线程执行:
dispatch_async(dispatch_get_main_queue(), ^{
// something
});
// 一次性执行:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// code to be executed once
});
// 延迟 2 秒执行:
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// code to be executed on the main queue after delay
});
5. 使用GCD来替代performSelector的原因:
5.1 performSelector 会导致内存泄漏问题
用performSelector:调用了一个方法,编译器并不知道将要调用的selector是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法用ARC的内存管理规则来判定返回值是不是该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而,这么做可能导致内存泄漏,因为方法在返回对象时已经将其保留了。
5.2 performSelector 返回值只能是void或对象类型(id类型)
如果想返回整数或浮点数等scalar类型值,那么就需要执行一些复杂的转换操作,而这种转换操作很容易出错。由于id类型表示指向任意Objective—C对象的指针,所以从技术上来讲,只要返回的大小和指针所占大小相同就行,也就是说,在32位架构的计算机上,可以返回任意32位大小的类型;而在64位架构的计算机上,则可以返回任意64位大小的类型。除此之外,还可以返回NSNumber进行转换…若返回的类型为C语言结构体,则不可使用performSelector方法。
5.3 performSelector 提供的方法局限性大
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
具备延后功能的那些方法无法处理带有两个参数的情况。而能够指定执行线程的哪些方法,则与之类似,所以也不是特别通用。如果要用这些方法,就得把很多参数打包到字典中,然后在被调用的方法中将这些参数提取出来,这样会增加开销,同时也提高了产生bug的可能性。
6. GCD 中block的使用
6.1 GCD 会对添加的Block进行复制
Dispatchqueue对添加的Block会进行复制,在完成执行后自动释放。换句话说,你不需要在添加 Block 到 Queue 时显式地复制
6.2 GCD 中的autorelease pool
GCD dispatch queue 有自己的autorelease pool来管理内存对象,但是不保证在什么时候会进行回收,如果在block中创建了大量的对象,可以添加自己的autorelease pool来进行管理。
6.3 GCD 中再开新的线程执行任务不一定更快
如果对于工作量小的block切换线程的开销,比直接在原来线程上执行block的开销要大,那么这样的话,会导致开新的线程反而没有原来执行的快,也就是说谁开销大谁慢。
6.4 GCD 的暂停和继续
1、dispatch_suspend 会暂停一个队列以阻止执行block对象,调用 dispatch_suspend 会增加queue的引用计数
2、dispatch_resume 会使得队列恢复继续执行block对象,调用 dispatch_resume 会减少queue的引用计数
挂起和继续是异步的,只在没有执行的block上生效,挂起一个block不会导致已经开始执行的block停止执行。
7. GCD中的信号量 Semaphore
7.1 信号量概念
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait函数就相当于来了一辆车,dispatch_semaphore_signal,就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value))调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就要把车停在这,所以就一直等下去。
7.2 信号量的创建和使用
1、创建 dispatch_semaphore_create
/*!
* @function dispatch_semaphore_create
使用信号量来处理多个线程之间竞争资源的情况特别合适,在value等于0的时候进行等待,在value大于0的时候运行
*
* @param value
* 初始化创建的信号量的个数
*
* @result
* 当前创建的信号量
*/
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT
DISPATCH_NOTHROW
dispatch_semaphore_t
dispatch_semaphore_create(long value);
2、等待 dispatch_semaphore_wait
/*!
* @function dispatch_semaphore_wait
*
* @abstract
* 等待一个信号量
*
* @discussion
* 会对信号量进行-1,如果value小于0,接下来的方法会允许等待的时间里一直等待直到有其他线程有信号量产生即 value>1 才开始执行
*
* @param dsema
* The semaphore. 不允许设置为NULL
* @param timeout
* 允许等待的超时时间
* 一下两个是宏定义的时间
* DISPATCH_TIME_NOW and DISPATCH_TIME_FOREVER constants.
*
*/
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
3、产生 dispatch_semaphore_signal
/*!
* @function dispatch_semaphore_signal
*
* @abstract
* 对信号量增加1
*
* @discussion
* 对信号量增加1,如果之前的value==0,那么这个操作会唤醒一个正在等待的线程
*
* @param dsema The counting semaphore.
* The semaphore. 不允许设置为NULL
*/
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
注意:dispatch_semaphore_signal 和 dispatch_semaphore_wait 必须成对出现,并且在 dispatch_release(aSemaphore);之前,aSemaphore 的value需要恢复之前的数值,不然会导致 EXC_BAD_INSTRUCTION
在ARC情况下不需要使用dispatch_release来进行释放,有系统统一管理
7.3 Dispatch Semaphore 的应用
1、控制并发线程数量
void dispatch_async_limit(dispatch_queue_t queue,NSUInteger limitSemaphoreCount, dispatch_block_t block) {
//控制并发数的信号量
static dispatch_semaphore_t limitSemaphore;
//专门控制并发等待的线程
static dispatch_queue_t receiverQueue;
//使用 dispatch_once而非 lazy 模式,防止可能的多线程抢占问题
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
limitSemaphore = dispatch_semaphore_create(limitSemaphoreCount);
receiverQueue = dispatch_queue_create("receiver", DISPATCH_QUEUE_SERIAL);
});
dispatch_async(receiverQueue, ^{
//有可用信号量后才能继续,否则等待
dispatch_semaphore_wait(limitSemaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
!block ? : block();
//在该工作线程执行完成后释放信号量
dispatch_semaphore_signal(limitSemaphore);
});
});
}
2、等待某个网络回调完之后才执行后面的操作
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[NetWorkkService queryCompletion:^(BOOL isSuccess) {
dispatch_semaphore_signal(sema);
} onError:^(int errorCode, NSString *errorMessage) {
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
// todo what you want to do after net callback
3、使用信号量来处理读写线程安全问题
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dispatch_queue_t queue = dispatch_queue_create("iKingsly", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
for (int i = 0; i < 1000; i++) {
dict[@(i)] = @(i);
}
dispatch_semaphore_signal(semaphore);
});
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
for (int i = 0; i < 1000; i++) {
NSLog(@"%@", dict[@(i)]);
}
dispatch_semaphore_signal(semaphore);
});
8. CGD Group
8.1 用group wait来等待queue的一组任务
如果要等待queue中的一系列操作完成后再去执行一个相应的任务,除了用barrier之外,我们也可以通过group来进行处理,dispatch group wait会阻塞当前的线程,直到group中的任务完成才会停止阻塞,这样我们可以达到一个目的,直到前面的任务完成了,才执行后面的代码
dispatch_queue_t queue = dispatch_queue_create("abc", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue,^{
NSLog(@"1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"2");
});
dispatch_group_async(group, queue, ^{
NSLog(@"3");
});
dispatch_group_async(group, queue, ^{
sleep(5);
NSLog(@"4");
});
// 开启一个异步队列来等待
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
NSLog(@"done");
});
});
NSLog(@"主线程");
log打印如下:
2017-03-22 22:43:59.946 ForDemo[2196:421544] 1
2017-03-22 22:43:59.946 ForDemo[2196:421543] 2
2017-03-22 22:43:59.946 ForDemo[2196:421546] 3
2017-03-22 22:43:59.946 ForDemo[2196:421405] 主线程
2017-03-22 22:44:05.018 ForDemo[2196:421549] 4
2017-03-22 22:44:05.018 ForDemo[2196:421549] done
8.2 用group notify来实现等待queue的一组任务
用 dispatch_group_notify方法可以等待group中的任务,notify中的任务在原来group中的任务执行结束前不会执行
dispatch_queue_t queue = dispatch_queue_create("abc", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue,^{
NSLog(@"1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"2");
});
dispatch_group_async(group, queue, ^{
NSLog(@"3");
});
dispatch_group_async(group, queue, ^{
sleep(5);
NSLog(@"4");
});
// 等待group中的任务执行完成
dispatch_group_notify(group, queue, ^{
NSLog(@"done");
});
NSLog(@"主线程");
log打印如下:
2017-03-22 22:48:28.632 ForDemo[2280:446210] 2
2017-03-22 22:48:28.632 ForDemo[2280:446146] 主线程
2017-03-22 22:48:28.632 ForDemo[2280:446196] 1
2017-03-22 22:48:28.632 ForDemo[2280:446197] 3
2017-03-22 22:48:33.698 ForDemo[2280:446199] 4
2017-03-22 22:48:33.698 ForDemo[2280:446199] done
8.3 手动进入group
dispatch_group_enter 手动通知 Dispatch Group 任务已经开始。你必须保证 dispatch_group_enter 和 dispatch_group_leave 成对出现,否则你可能会遇到诡异的崩溃问题。
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(5);
NSLog(@"任务一完成");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(8);
NSLog(@"任务二完成");
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务完成");
});
log打印如下:
2017-03-22 23:04:31.093 ForDemo[2476:497726] 任务一完成
2017-03-22 23:04:34.086 ForDemo[2476:497725] 任务二完成
2017-03-22 23:04:34.086 ForDemo[2476:497725] 任务完成
9. Dispatch Source
9.1 它有什么用?
dispatch source 的作用是负责监听事件,先看看它的构造函数。
dispatch_source_create(dispatch_source_type_t type,
uintptr_t handle,
unsigned long mask,
dispatch_queue_t queue);
第1个参数:要监听的事件类型
第2个参数:可以理解为句柄、索引或id,假如要监听进程,需要传入进程的ID
第3个参数:根据参数2,可以理解为描述,提供更详细的描述,让它知道具体要监听什么
第4个参数:当事件发生时,将block添加至哪个队列来执行
9.2 可监听事件的类型
DISPATCH_SOURCE_TYPE_TIMER 定时响应
DISPATCH_SOURCE_TYPE_SIGNAL 接收到UNIX信号时响应
DISPATCH_SOURCE_TYPE_READ IO操作,如对文件的操作、socket操作的读响应
DISPATCH_SOURCE_TYPE_WRITE IO操作,如对文件的操作、socket操作的写响应
DISPATCH_SOURCE_TYPE_VNODE 文件状态监听,文件被删除、移动、重命名
DISPATCH_SOURCE_TYPE_PROC 进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号
DISPATCH_SOURCE_TYPE_MACH_SEND
DISPATCH_SOURCE_TYPE_MACH_RECV 上面2个都属于Mach相关事件响应
DISPATCH_SOURCE_TYPE_DATA_ADD
DISPATCH_SOURCE_TYPE_DATA_OR 上面2个都属于自定义的事件,并且也是有自己来触发
9.3 怎么使用
1、ADD类型事件监听
自定义事件(DISPATCH_SOURCE_TYPE_DATA_ADD、DISPATCH_SOURCE_TYPE_DATA_OR),先看代码
dispatch_source_t source =dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(source,^{
NSLog(@"监听函数:%lu",dispatch_source_get_data(source));
});
dispatch_resume(source);
dispatch_queue_t myqueue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, NULL);
dispatch_async(myqueue, ^ {
int i;
for(i = 0;i < 4;i++){
dispatch_source_merge_data(source,i);
}
});
首先使用dispatch_source_create函数创建 dispatchsource,第1个参数表示它是一个自定义的_ADD类型的监听,具体作用后面说,2、3参数这里面没有作用设置为0即可,第4个参数 表示一旦事件触发就将要执行的代码块添加到主队列中执行,接着我们使用dispatch_source_set_event_handler函数为这个监 听设置事件的源和响应体,第1个参数表示这个监听是响应用户自定义事件的,也就是我们上面定义的dispatchsource,第2个参数是负责响应的代 码块。很有意思的是当我们创建监听后,这个监听默认是挂起的,需要手动恢复,所以我们使用dispatch_resume函数恢复这个监听,为了测试这个监听,我们后面又通过for循环触发事件,触发事件的函数就是dispatch_source_merge_data,这个函数负责触发自定义事件,第1 个参数表示要触发哪个监听,第2个参数是向监听传入一个unsigned long 类型的值, 我们这里传入循环的索引,好了,整体来看这段程序,dispatch_source_merge_data函数会被执行4次,并分别传入0、1、2、3这 4个值,既然dispatch_source_merge_data负责触发事件,那么我们在监听里面的响应体应该会监听到,结果也确实监听到了,但是并不是我们想象的那样打印4次,而是只打印了一次,打印结果是4次传入值相加的和,也就是6,这就是 DISPATCH_SOURCE_TYPE_DATA_ADD参数的作用,这个监听在创建之初就被设置为自定义监听,并且会把监听结果相加,然后统一响 应。这里你应该会奇怪,既然结果会相加并统一响应,那跟触发的时候加好,然后触发一次有什么区别呢,好吧,我们把触发事件的for循环改一下,然后再运 行,看看会发生什么
for(i = 0;i < 4;i++){
dispatch_source_merge_data(source,i);
[NSThread sleepForTimeInterval:0.0001];
}
我们在触发事件的地方加上0.0001秒的延迟,然后运行整个程序多次,你会发现奇怪 的现象,我们同样是触发4次事件,但是响应的次数变成不确定了,可能是1次,也可能是2次,如果你将延迟时间设置长点,甚至设置为0点几秒就能让响应的次 数变为固定的4次,为什么会这样呢,其实这就是这个自定义事件设计的初衷。如果同一时间同一个事件被触发的频率非常密集,那么 dispatchsource会将这些密集的响应相加统计做出响应,但是如果触发的相对零散,那么dispatch source会分别进行响应,这其实是在智能的控制UI的没必要的更新操作,因为那些几乎在同一时间更新进度条的操作完全可以统一进行更新,没有必要每次 都更新一下。这样做也会减少UI线程的负担,例如更新进度条的同时,你的UI可能还在同时响应用户的输入、触碰等工作。当然你可以选择实时更新,办法就是 直接使用使用dispatch_async直接更新界面。Dispatch source在统一响应完毕后计数变为0,后面再触发的会重新相加。DISPATCH_SOURCE_TYPE_DATA_OR会将所有监听到的值逻辑与操作,然后统一触发。貌似没有DISPATCH_SOURCE_TYPE_DATA_ADD常用
2、使用Dispatch Queue 来取代NSTimer
众所周知,定时器有NSTimer,但是NSTimer有如下弊端:
必须保证有一个活跃的runloop,子线程的runloop是默认关闭的。这时如果不手动激活runloop,performSelector和scheduledTimerWithTimeInterval的调用将是无效的。
NSTimer的创建与撤销必须在同一个线程操作、performSelector的创建与撤销必须在同一个线程操作。
内存管理有潜在泄露的风险会造成循环引用
所以我们可以使用 Dispatch Source 的 DISPATCH_SOURCE_TYPE_TIMER 来实现这个效果:
- (void) startGCDTimer{
NSTimeInterval period = 1.0; //设置时间间隔
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), period * NSEC_PER_SEC, 0); //每秒执行
dispatch_source_set_event_handler(_timer, ^{
//在这里执行事件
NSLog(@"每秒执行test");
});
dispatch_resume(_timer);
}
-(void) pauseTimer{
if(_timer){
dispatch_suspend(_timer);
}
}
-(void) resumeTimer{
if(_timer){
dispatch_resume(_timer);
}
}
-(void) stopTimer{
if(_timer){
dispatch_source_cancel(_timer);
_timer = nil;
}
}
3、监控文件系统对象
设置DISPATCH_SOURCE_TYPE_VNODE 类型的Dispatch Source,可以从这个 Source 中接收文件删除、写入、重命名等通知。
int fd = open(filename, O_EVTONLY);
if (fd == -1)
return NULL;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE,
, DISPATCH_VNODE_RENAME, queue);
if (source)
{
// 保持文件名
int length = strlen(filename);
char* newString = (char*)malloc(length + 1);
newString = strcpy(newString, filename);
dispatch_set_context(source, newString);
// 设置Source的handler 来对监测文件修改的处理
dispatch_source_set_event_handler(source, ^{
const char* oldFilename = (char*)dispatch_get_context(source);
MyUpdateFileName(oldFilename, fd);
});
// 做释放source之前的处理 关闭文件
dispatch_source_set_cancel_handler(source, ^{
char* fileStr = (char*)dispatch_get_context(source); free(fileStr);
close(fd);
});
// 开始执行start
dispatch_resume(source);
}
4、监测进程的变化
进程 dispatch source 可以监控特定进程的行为,并适当地响应。父进程可以使用 dispatch source 来监控自己创建的所有子进程,例如监控子进程的死亡;类似地,子进程也可以使用 dispatch source 来监控父进程,例如在父进程退出时自己也退出。
NSRunningApplication *mail = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.mail"];
if (mail == nil) {
return;
}
pid_t const pid = mail.processIdentifier;
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pid, DISPATCH_PROC_EXIT, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(self.source, ^(){
NSLog(@"Mail quit.");
});
//在事件源传到你的事件处理前需要调用dispatch_resume()这个方法
dispatch_resume(self.source);
5、监视文件夹内文件变化
NSURL *directoryURL; // assume this is set to a directory
int const fd = open([[directoryURL path] fileSystemRepresentation], O_EVTONLY);
if (fd < 0) {
char buffer[80];
strerror_r(errno, buffer, sizeof(buffer));
NSLog(@"Unable to open \"%@\": %s (%d)", [directoryURL path], buffer, errno);
return;
}
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fd,
DISPATCH_VNODE_WRITE | DISPATCH_VNODE_DELETE, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^(){
unsigned long const data = dispatch_source_get_data(source);
if (data & DISPATCH_VNODE_WRITE) {
NSLog(@"The directory changed.");
}
if (data & DISPATCH_VNODE_DELETE) {
NSLog(@"The directory has been deleted.");
}
});
dispatch_source_set_cancel_handler(source, ^(){
close(fd);
});
self.source = source;
dispatch_resume(self.source);
//还要注意需要用DISPATCH_VNODE_DELETE 去检查监视的文件或文件夹是否被删除,如果删除了就停止监听
10. Dispatch Barrier
dispatch_barrier 最大的作用就是用来做阻塞,阻塞当前的线程,做到一个承上启下的作用,只有在它之前的任务全部执行完之后,它和它之后的任务才能进行。可以理解为成语一夫当关,万夫莫开,只有在它面前的任务“死掉了”(即执行完了)后面的任务才能继续进行下去。
使用dispatch_barrier 是用来阻断并行任务不能确定先后任务完成的问题,它必须使用在自定义并行队列上,否则没有意义,为什么说没意义呢,我们接下来分析为什么没意义:
如果运用在串行队列上,没有意义,因为串行队列本来就是先进先出的规则,用了栅栏跟没用没有区别
如果使用全局队列,也是没有意义,我们每次 dispatch_get_global_queue 获取到的队列都是不同的,我们任务前后执行不在同一个线程上,也就没有了截流之分。
系统中提供的可变对象都是线程不安全的,也就是在一个线程进行写入数据的时候,不允许其他线程访问,无论是读或者是写都是不允许的。使用dispatch_barrier来实现写入安全:
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dispatch_queue_t queue = dispatch_queue_create("iKingsly", DISPATCH_QUEUE_CONCURRENT);
dispatch_barrier_async(queue, ^{
for (int i = 0; i < 1000; i++) {//这边写入完成,下面才能开始读字典里的数据
dict[@(i)] = @(i);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 1000; i++) {
NSLog(@"%@", dict[@(i)]);
}
});
11. GCD死锁
当前串行队列里面同步执行当前串行队列就会死锁,解决的方法就是将同步的串行队列放到另外一个线程就能够解决。
- (void)deadLockCase1 {
NSLog(@"1");
//主队列的同步线程,按照FIFO的原则(先入先出),2排在3后面会等3执行完,但因为同步线程,3又要等2执行完,相互等待成为死锁。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
}
- (void)deadLockCase2 {
NSLog(@"1");
//3会等2,因为2在全局并行队列里,不需要等待3,这样2执行完回到主队列,3就开始执行
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"2");
});
NSLog(@"3");
}
- (void)deadLockCase3 {
dispatch_queue_t serialQueue = dispatch_queue_create("com.starming.gcddemo.serialqueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(serialQueue, ^{
NSLog(@"2");
//串行队列里面同步一个串行队列就会死锁
dispatch_sync(serialQueue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
- (void)deadLockCase4 {
NSLog(@"1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
//将同步的串行队列放到另外一个线程就能够解决
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
- (void)deadLockCase5 {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1");
//回到主线程发现死循环后面就没法执行了
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
});
NSLog(@"4");
//死循环
while (1) {
//
}
}
12. GCD实际使用
1、FMDB如何使用dispatch_queue_set_specific和dispatch_get_specific来防止死锁,作用类似objc_setAssociatedObject跟objc_getAssociatedObject
static const void * const kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey;
//创建串行队列,所有数据库的操作都在这个队列里
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
//标记队列
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
//检查是否是同一个队列来避免死锁的方法
- (void)inDatabase:(void (^)(FMDatabase *db))block {
FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock");
}
2、DTCoreText使用GCD加快解析速度
DTCoreText采用的是SAX解析,iOS自带了XML/HTML的解析引擎libxml,提供了两个解析接口,DOM解析和SAX解析,前者使用简单但是占用内存多,SAX解析由于不会返回一个dom树,采用的是查到一个标签比如回调startElement方法碰到内容就回调_characters碰到类似就回调endElement这样的方式。
根据这种解析方式DTCoreText使用多线程解析能够更快的解析,DTHTMLAttributedStringBuilder使用三个dispatch_queue
_dataParsingQueue:解析html的
_treeBuildingQueue:生成dom树的
_stringAssemblyQueue:组装NSAttributeString的 获取三个队列全部完成采用了dispatch_group的dispatch_group_wait这种阻塞同步方式来返回结果。