GCD使用汇总
dispatch_queue、dispatch_block
dispatch_block_t实际上是一个自定义block类型typedef void (^dispatch_block_t)(void);
苹果对于它的注释相当有意思:
如果不使用ARC,分配在堆上或者复制到堆上的block对象一定要通过发送release消息或者调用Block_release函数来释放。 block字面量的声明是在栈空间上,一定要杜绝这样的创建:
dispatch_block_t block;
if (x) {
block = ^{ printf("true\n"); };
} else {
block = ^{ printf("false\n"); };
}
block(); // unsafe!!!
这样其实相当于
if (x) {
struct Block __tmp_1 = ...; // setup details
block = &__tmp_1;
} else {
struct Block __tmp_2 = ...; // setup details
block = &__tmp_2;
}
这个示范中,栈变量的地址脱离了它声明时的作用域。这是一个经典的C语言bug。 取而代之的应该是:block字面量必须通过Block_copy函数或者发送copy消息复制到堆上。
dispatch_queue_t是任务执行的队列类型,它通过dispatch_queue_create创建,有两种类型
DISPATCH_QUEUE_SERIAL(NULL): 串行队列
DISPATCH_QUEUE_CONCURRENT: 并行队列
串行队列中的任务是有序执行的,并行队列中的任务是无序执行的,具体还要看以同步方式执行还是以异步方式执行。
两个特殊的队列:
main quue: dispatch_get_main_queue()获取。
global queue: dispatch_get_global_queue(0, 0)获取,这个函数的第一个参数是服务质量;第二个参数为保留值,始终传0.这个队列又叫做全局并行队列。
这两个队列的特点是:
放在主队列中的任务一定在主线程中执行。
放在全局队列中的任务一定是在子线程中执行。(大部分情况是对的,但是dispatch_sync方法优化为:使用当前线程)
dispatch_sync dispatch_async
这部分通过官方文档和一些任务执行顺序分析来理解这两个方法的区别,顺序分析时要注意以下几个因素:1.环境:当前所处的线程
2.队列:将任务添加到了哪种类型的队列
3.执行方式:同步还是异步
①dispatch_sync
这个方法阻塞,也就是说任务一定是添加到队列中之后并且执行完成之后,程序才会继续运行。
苹果的文档说的很清楚:Submits a block object for execution on a dispatch queue and waits until that block completes.
Submits a block to a dispatch queue for synchronous execution. Unlike dispatch_async, this function does not return until the block has finished. Calling this function and targeting the current queue results in deadlock.。因此下面的程序无论换成什么队列执行结果相同:
以下环境都是在主线程中
dispatch_queue_t serialQueue = dispatch_queue_create("com.mikezh.serial.test", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.mikezh.concurrent.test", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
NSLog(@"begin");
for (NSUInteger i = 0; i < 10; i++) {
dispatch_sync(concurrentQueue/*serialQueue globalQueue 都是一样的*/, ^{
if (i == 2) {
sleep(2);
}
if (i == 5) {
sleep(5);
}
NSLog(@"任务%zd, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end");
begin
任务0, <NSThread: 0x600000077f80>{number = 1, name = main}
任务1, <NSThread: 0x600000077f80>{number = 1, name = main}
任务2, <NSThread: 0x600000077f80>{number = 1, name = main}
任务3, <NSThread: 0x600000077f80>{number = 1, name = main}
任务4, <NSThread: 0x600000077f80>{number = 1, name = main}
任务5, <NSThread: 0x600000077f80>{number = 1, name = main}
任务6, <NSThread: 0x600000077f80>{number = 1, name = main}
任务7, <NSThread: 0x600000077f80>{number = 1, name = main}
任务8, <NSThread: 0x600000077f80>{number = 1, name = main}
任务9, <NSThread: 0x600000077f80>{number = 1, name = main}
end
这里global queue执行代码调度的线程取决于环境(可见global queue中任务一定是在子线程执行这个说法是错误的,这个在于dispatch_sync方法的优化:As an optimization, this function invokes the block on the current thread when possible.),例如:
dispatch_async(globalQueue, ^{
NSLog(@"begin");
for (NSUInteger i = 0; i < 10; i++) {
dispatch_sync(globalQueue, ^{
if (i == 2) {
sleep(2);
}
if (i == 5) {
sleep(5);
}
NSLog(@"任务%zd, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end");
});
begin
任务0, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任务1, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任务2, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任务3, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任务4, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任务5, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任务6, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任务7, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任务8, <NSThread: 0x61800006f880>{number = 3, name = (null)}
任务9, <NSThread: 0x61800006f880>{number = 3, name = (null)}
end
另外,苹果也说明了什么情况下会造成死锁:在currentqueue中调用dispatch_sync方法,并且将任务添加到currentqueue中,也就是说下面的代码会造成死锁:
// 主线程、主队列、同步执行=====>死锁
dispatch_sync(mainQueue, ^{
});
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{
});
});
③dispatch_async
向queue提交异步执行的block并立即返回。
这个函数是向队列提交block的基本机制。
调用这个函数总是在提交block之后立即返回并且从不等待block的调用。
目标queue会参照其他的block来决定block是串行还是并行执行。相互独立的串行队列参照别的串行队列来并行处理。
参数:queue
block提交到的queue.这个queue会被系统持有直到block运行完毕。
block
提交到目标调度queue中的block。这个函数会帮你执行Block_copy和Block_release。
dispatch_async可以用来分析任务执行时要考虑的就是:block提交的顺序,block开始执行的顺序。
1.环境中后面的代码不会等待block的执行
2.对于串行队列而言,block执行的顺序只能和执行的顺序相同,
对于并行队列而言,因为任务的执行是并行的,所以产生提交的block顺序和执行的顺序不一致的情况。
对于以下程序:
NSLog(@"begin");
for (NSUInteger i = 0; i < 10; i++) {
dispatch_async(queue, ^{
if (i == 2) {
sleep(2);
}
if (i == 5) {
sleep(5);
}
NSLog(@"任务%zd, %@", i, [NSThread currentThread]);
});
}
NSLog(@"end");
如果queue是mainQueue
begin
end
任务0, <NSThread: 0x60000006de80>{number = 1, name = main}
任务1, <NSThread: 0x60000006de80>{number = 1, name = main}
任务2, <NSThread: 0x60000006de80>{number = 1, name = main}
任务3, <NSThread: 0x60000006de80>{number = 1, name = main}
任务4, <NSThread: 0x60000006de80>{number = 1, name = main}
任务5, <NSThread: 0x60000006de80>{number = 1, name = main}
任务6, <NSThread: 0x60000006de80>{number = 1, name = main}
任务7, <NSThread: 0x60000006de80>{number = 1, name = main}
任务8, <NSThread: 0x60000006de80>{number = 1, name = main}
任务9, <NSThread: 0x60000006de80>{number = 1, name = main}
如果是serialQueue
begin
end
任务0, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任务1, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任务2, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任务3, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任务4, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任务5, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任务6, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任务7, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任务8, <NSThread: 0x61000026a000>{number = 3, name = (null)}
任务9, <NSThread: 0x61000026a000>{number = 3, name = (null)}
如果是globalQueue
begin
end
任务0, <NSThread: 0x60800007d6c0>{number = 3, name = (null)}
任务1, <NSThread: 0x61000007ca80>{number = 4, name = (null)}
任务4, <NSThread: 0x618000261040>{number = 6, name = (null)}
任务3, <NSThread: 0x60000007f780>{number = 5, name = (null)}
任务6, <NSThread: 0x60000007e0c0>{number = 7, name = (null)}
任务7, <NSThread: 0x61000007bfc0>{number = 8, name = (null)}
任务8, <NSThread: 0x60800007d6c0>{number = 3, name = (null)}
任务9, <NSThread: 0x60800007d540>{number = 9, name = (null)}
任务2, <NSThread: 0x60800007d8c0>{number = 10, name = (null)}
任务5, <NSThread: 0x600000262b00>{number = 11, name = (null)}
如果是concurrentQueue
begin
end
任务1, <NSThread: 0x610000073a40>{number = 4, name = (null)}
任务0, <NSThread: 0x618000065040>{number = 3, name = (null)}
任务3, <NSThread: 0x600000068f80>{number = 5, name = (null)}
任务4, <NSThread: 0x618000067bc0>{number = 6, name = (null)}
任务6, <NSThread: 0x60800006ec80>{number = 7, name = (null)}
任务7, <NSThread: 0x6180000679c0>{number = 8, name = (null)}
任务8, <NSThread: 0x61000006cdc0>{number = 9, name = (null)}
任务9, <NSThread: 0x610000073a40>{number = 4, name = (null)}
任务2, <NSThread: 0x610000073c40>{number = 10, name = (null)}
任务5, <NSThread: 0x618000064d40>{number = 11, name = (null)}
dispatch_set_target_queue、dispatch_object_t
dispatch_set_target_queue的作用是:为指定的object设置目标队列。 目标队列负责处理这个object。object最后执行所在的队列取决于目标队列。另外,修改目标队列会影响一些object的行为:object为Dispatch queues:
这个object,也就是这个queue会继承目标队列的优先级。可以使用dispatch_get_global_queue函数获取一个有期待的优先级的合适的目标队列。
如果你向串行队列提交block,同时这个串行队列的目标队列是一个不同的串行队列,这个block相对于已经提交到目标队列中的其他block不会异步执行,对于设置同样目标队列的其他队列中的block也不会异步执行。
Important
如果你为一个队列修改了目标队列,你必须小心以避免创建队列层级的循环(目标环)。
object为Dispatch sources:
目标队列为source指定了它的事件处理和取消处理的block将会提交到哪里。
object为Dispatch I/O channels:
目标队列为I/O channel指定了I/O操作将在哪里执行。这会影响到I/O操作的优先级。比如,如果channel的目标队列优先级设置为DISPATCH_QUEUE_PRIORITY_BACKGROUND,dispatch_io_read和dispatch_io_write函数进行的任何I/O操作都会在发生资源竞争时停止。
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, globalQueue);
dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"hahah--%@", [NSThread currentThread]);
});
dispatch_set_target_queue(self.timer, mainQueue); // timer在主线程上执行
dispatch_resume(self.timer);
dispatch_set_target_queue方法的第一个参数object是dispatch_object类型却可以传递多种类型,这是为什么呢?
dispatch_source_t是如何定义的
// source.h
DISPATCH_SOURCE_DECL(dispatch_source);
// object.h // 非swift环境下
DISPATCH_DECL(name);
// object.h 非swift环境下
#define DISPATCH_DECL(name) OS_OBJECT_DECL_SUBCLASS(name, dispatch_object)
// object.h 非swift环境下
#define OS_OBJECT_DECL_SUBCLASS(name, super) \
OS_OBJECT_DECL_IMPL(name, <OS_OBJECT_CLASS(super)>)
// 下面的①②③是对这个宏的展开
#define OS_OBJECT_DECL_IMPL(name, ...) \
OS_OBJECT_DECL_PROTOCOL(name, __VA_ARGS__) \
typedef NSObject<OS_OBJECT_CLASS(name)> \
* OS_OBJC_INDEPENDENT_CLASS name##_t
// ①
#define OS_OBJECT_DECL_PROTOCOL(name, ...) \
@protocol OS_OBJECT_CLASS(name) __VA_ARGS__ \
@end
// ②
#define OS_OBJECT_CLASS(name) OS_##name
// ③
#if __has_attribute(objc_independent_class)
#define OS_OBJC_INDEPENDENT_CLASS __attribute__((objc_independent_class))
#endif // __has_attribute(objc_independent_class)
#ifndef OS_OBJC_INDEPENDENT_CLASS
#define OS_OBJC_INDEPENDENT_CLASS
#endif
因此dispatch_source_t完全展开就是:
@protocol OS_dispatch_source <OS_dispatch_object>
@end
typedef NSObject<OS_dispatch_source>* dispatch_source_t
dispatch_io_t是如何定义的
DISPATCH_DECL(dispatch_io);
OS_OBJECT_DECL_SUBCLASS(name, dispatch_object)
// ...
因此dispatch_source_t完全展开就是:
@protocol OS_dispatch_io <OS_dispatch_object>
@end
typedef NSObject<OS_dispatch_io>* dispatch_io_t
dispatch_queue_t就是
@protocol OS_dispatch_queue <OS_dispatch_object>
@end
typedef NSObject<OS_dispatch_queue>* dispatch_queue_t
类似的还有dispatch_semaphore、dispatch_data_t、dispatch_group_t这几个类型。
他们都是一个遵守相应协议的NSObject对象类型,这些协议的基协议OS_dispatch_object就是由dispatch_object_t声明的:
OS_OBJECT_DECL_CLASS(dispatch_object);
#define OS_OBJECT_DECL_CLASS(name) \
OS_OBJECT_DECL(name)
#define OS_OBJECT_DECL(name, ...) \
OS_OBJECT_DECL_IMPL(name, <NSObject>)
#define OS_OBJECT_DECL_IMPL(name, ...) \
OS_OBJECT_DECL_PROTOCOL(name, __VA_ARGS__) \
typedef NSObject<OS_OBJECT_CLASS(name)> \
* OS_OBJC_INDEPENDENT_CLASS name##_t
完全展开就是:
@protocol OS_dispatch_object <NSObject>
@end
typedef NSObject<OS_dispatch_object> * dispatch_object_t
dispatch_after dispatch_time_t
dispatch_after函数会在指定的时刻将block异步地添加到指定的队列。 支持传递DISPATCH_TIME_NOW作为when参数,但是不如调用dispatch_async更优。不可以传递DISPATCH_TIME_FOREVER。要注意的是:
并不是在指定的时间后执行处理,而是在指定时间追加block到队列中,因为mainQueue在主线程的runloop中执行,所以在比如每隔1/60秒执行的RunLoop中。block最快在指定时刻执行,最慢在指定时刻+1/60秒执行,并且在main queue中有大量处理追加或主线程的处理本身有延迟时,这个时间会更长。
这个方法的第一个参数是dispatch_time_t类型,它实际上是:
typedef uint64_t dispatch_time_t;
它是对时间的一个抽象表示,0代表现在, DISPATCH_TIME_FOREVER代表着无限大,可以通过两个函数创建
/// 根据默认时钟创建一个时间,或者修改一个已存在的时间
/// OS X 默认时钟基于mach_absolute_time()函数
/// 参数when:需要修改的时间,如果传递0,这个函数会使用mach_absolute_time()返回值。
/// 参数delta:要添加的纳秒数
dispatch_time_t
dispatch_time(dispatch_time_t when, int64_t delta);
/// 使用系统时间创建一个时间类型值
/// OS X 系统时钟基于gettimeofday(3)函数
/// 参数when:需要修改的时间或依据时间,是一个struct timespec类型指针,如果传递NULL,这个函数会使用gettimeofday(3)返回值。
/// 参数delta:要添加的纳秒数
dispatch_time_t
dispatch_walltime(const struct timespec *_Nullable when, int64_t delta);
dispatch_group
group对于多个任务结束后执行一些操作非常有用。Apple这样介绍: 一组block对象提交到一个队列中来异步调用。group是一个管理一系列block的机制。你的程序可以根据需要同步或者异步监控这些block。另外,group对于一些依赖其他操作完成的同步代码非常有用。 要注意的是:group中的block可以在不同的queue中执行,每一个block中可以添加更多block到group中 group会追踪有多少个未完成的block,GCD会持有group,直到所有相关的block全部执行完成。 举个例子:下载A、B、C三个文件,全部下载完成之后提示dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, globalQueue, ^{
NSLog(@"downloading A ...");
});
dispatch_group_async(group, globalQueue, ^{
NSLog(@"downloading B ...");
});
dispatch_group_async(group, globalQueue, ^{
NSLog(@"downloading C ...");
});
dispatch_group_notify(group, mainQueue, ^{
NSLog(@"下载完成");
});
如果是想等待全部执行完成之后再进行其他的代码可以使用
dispatch_group_wait(group, time)
注意这个方法是阻塞方法,它有个特点:会一直等待直到到达等待的时间 或 任务执行完成才返回。
对于它的返回值,如果返回值为0,代表group中的任务已经全部完成,非0则没有完成。
但实际开发中有这样的场景,比如进行一组网络请求任务,每一个任务都是异步任务,而请求完成并将数据解析完毕我们才认为是任务的完成,而这些过程又有可能是跨线程的。这时候就要使用dispatch_group_enter和dispatch_group_leave组合,通过它们可以对group进行更细粒度的控制。这两个函数都是线程安全的,对应着添加和移除任务,因此使用时必须要成对出现。
dispatch_group_enter(group);
dispatch_group_async(group, globalQueue, ^{
dispatch_async(globalQueue, ^{
NSLog(@"downloading A ...");
dispatch_group_leave(group);
});
});
dispatch_group_async(group, globalQueue, ^{
dispatch_group_enter(group);
dispatch_async(globalQueue, ^{
NSLog(@"downloading B ...");
dispatch_group_leave(group);
});
});
dispatch_group_enter(group);
dispatch_group_async(group, globalQueue, ^{
dispatch_async(globalQueue, ^{
NSLog(@"downloading C ...");
dispatch_group_leave(group);
});
});
dispatch_group_notify(group, mainQueue, ^{
NSLog(@"下载完成");
});
dispatch_barrier
我们知道在对于并行队列,使用async方法执行任务,任务被添加到队列中是有序的,但是执行无序。但有这么一个场景:对于数据读操作并发执行没有问题,可对于写操作来说却要控制写过程中不再进行读操作,以避免数据竞争问题。类似的场景很多,大体归结为在许多并发任务中,有1个任务在执行的时候必须保证其他的任务等待其执行完毕.诸如此类的问题可以使用dispatch_barrier_asyn函数解决。dispatch_barrier_async
提交一个异步执行的barrier block并立即返回。
调用这个函数总是在block被提交之后立即返回,而从不等待block的执行。当barrier block到达自定义并发队列的队头时,它不会被立即执行。它会等待直到当前正在执行的lock执行完毕,到这时,barrier block才会执行。
任何在barrier block后套面提交的block也不会执行,直到barrier block执行完毕。
你指定的队列应当是一个使用dispatch_queue_create函数自己创建的并行队列。如果传给这个函数一个串行队列或
global并行队列,这个函数会像dispatch_async函数一样。
下图可以很好的说明这个函数的作用
dispatch_async(concurrentQueue, ^{
NSLog(@"1");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"2");
});
dispatch_barrier_async(concurrentQueue, ^{
sleep(5);
NSLog(@"barrier");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"3");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"4");
});
可以测试打印结果:
2
1
barrier
3
4
// 1、2无序一定在之前, 3、4无序一定在之后
dispatch_barrier_sync
提交一个barrier block并等待这个block执行完毕。
提交一个barrier block到队列用来同步执行。不同于dispatch_barrier_async,这个函数直到barrier block执行完毕才返回。目标队列是当前队列会发生死锁。当barrier block到达自定义并发队列的队头时,它不会被立即执行。它会等待直到当前正在执行的lock执行完毕,到这时,barrier block才会执行。任何在barrier block后套面提交的block也不会执行,直到barrier block执行完毕。
你指定的队列应当是一个使用dispatch_queue_create函数自己创建的并行队列。如果传给这个函数一个串行队列或
global并行队列,这个函数会像dispatch_sync函数一样。
不同于dispatch_barrier_async,系统不会持有目标队列。因为调用这个函数是同步的,它借用了调用着的引用。而且也不会对block进行Block_copy操作。
作为优化,这个函数尽可能在当前线程调用barrier block
可以看到:最后面的优化说明和dispatch_sync方法完全一致。
这个函数和dispatch_barrier_async的区别就在于能否阻塞当前的线程。
测试:
dispatch_async(concurrentQueue, ^{
NSLog(@"1");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"2");
});
// 阻塞
dispatch_barrier_sync(concurrentQueue, ^{
sleep(5); // 如果当前环境为主线程,则界面frozen
NSLog(@"barrier sync执行");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"3");
});
dispatch_async(concurrentQueue, ^{
NSLog(@"4");
});
dispatch_apply
为执行多个操作向队列提交一个block,并且在返回之前等待所有的操作完成。如果目标队列是dispatch_get_global_queue返回的并行队列,block会被并行执行,因此它必须是可重入安全的。配合并行队列使用这个方法对于一个循环的高效并发来说非常有用。
NSLog(@"begin");
dispatch_apply(5, globalQueue, ^(size_t index) {
NSLog(@"%zd", index);
});
NSLog(@"end");
结果为:
begin
0
1
2
4
3
end
可以利用这个方法高效地处理数组中的数据,不过要注意:虽然会等待所有的任务执行完成才返回,但每个任务的执行是异步无序的。
dispatch_apply(array.count, globalQueue, ^(size_t index) {
id element = array[index];
// handler
});
dispatch_suspend、dispatch_resume
dispatch_suspend
暂停在dispatch object上的block的执行。
通过暂停一个dispatch object,你的程序可以暂时阻止与这个object有关的任何block的执行。这个暂停发生在调用方法时所有正在执行的block完成之后。调用这个函数会递增object的暂停数,而调用dispatch_resume会少这个计数,所以你必须用一个相匹配的dispatch_resume调用来平衡每一次的dispatch_suspend调用。
一旦object被恢复,任何提交到队列中的block或者通过dispatch source观察的事件就会执行。
dispatch_suspend(queue)
dispatch_suspend(timer)
dispatch_resume
继续执行dispatch object上的block。
调用这个方法会递减暂停的队列数或暂停的事件源对象。当计数大于0时,对象会保持暂停。当暂停数重置为0,任何提交到队列中的block或者通过dispatch source观察的事件会被执行。
有一个例外:每次调用dispatch_resume必须是来平衡调用dispatch_suspend的。新的通过dispatch_source_create函数返回的事件源对象有一个值为1暂停计数,因此必须在事件执行之前resume。这个方法使你的程序在事件分发之前完整地配置事件源对象。对于其他情况,都不允许比调用dispatch_suspend的次数多,那会导致负的暂停计数。
dispatch_resume(queue)
dispatch_resume(timer)
dispatch_semaphore
semaphore即信号量。 在多道程序环境下,操作系统如何实现进程之间的同步和互斥显得极为重要。荷兰学者Dijkstra给出了一种解决并发进程间互斥与同步关系的通用方法,即信号量机制。 信号量是一个具有非负初值的整型变量,并且有一个队列与它关联。信号量除初始化外,仅能通过P、V 两个操作来访问,这两个操作都由原语组成,即在执行过程中不可被中断,也就是说,当一个进程在修改某个信号量时, 没有其他进程可同时对该信号量进行修改。P操作信号量减1,如果信号量≥0,表示可以继续执行;如果<0就要阻塞等待,直到信号量>=0。V操作信号量加1。信号量可以模拟现实中类似于通行证的概念,即信号量>=0可以通行,而信号量<0时则需要等待增加才可以以通行。因此信号量机制编程必涉及三个函数,创建信号量、增加信号量、减少信号量。
dispatch_semaphore_t
dispatch_semaphore_create(long value)
创建一个信号量,参数为初始值。
传入0适用于两个线程需要解决对于一个资源的竞争。
传入一个大于0的值适用于管理一个有限的资源池,这个池子的大小与传入的值相等。
当你的程序不再需要信号量时,应该调用dispatch_release来释放它对信号量对象的引用并最终释放内存。(ARC会帮助处理,因此不要手动调用dispatch_release()函数)
long
dispatch_semaphopre_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
等待 (减少)一个信号量.
递减信号量计数。如果递减之后的结果值小于0,这个方法会在返回之前一直等待一个signal信号。
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
signal或者说递增一个信号量。
递增信号量计数。如果之前的值小于0,这个函数会异步地唤起一个正在dispatch_semaphore_wait函数中等待的线程。
模拟一次资源竞争的问题,在多个线程中操作同一个数据是非常常见的资源竞争的情况,这样非常容易引起数据不一致,有时候应用会异常结束。我们使用dispatch_barrier_async可以解决这个问题,但是它是对整块block任务的隔离,而并没有细微到对要操作的数据这个粒度的限制。例如使用可变数组模拟多线程写数据的情况(只是模拟写数据过程,不考虑顺序),
NSUInteger count = 10;
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:count];
for (NSUInteger i = 0; i < count; i++) {
dispatch_async(globalQueue, ^{
[mutableArray addObject:[NSNumber numberWithInteger:i]];
});
}
在多线程中更新NSMutableArray, 这段代码异常率是极高的。可以使用信号量机制进行保证线程安全性,任何一个正在写的操作必须要完成之后,才能进行下一个写的操作:
NSUInteger count = 10;
self.mutableArray = [NSMutableArray arrayWithCapacity:count];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (NSUInteger i = 0; i < count; i++) {
dispatch_async(globalQueue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[self.mutableArray addObject:[NSNumber numberWithInt:i]]; // 执行到这里说明没有阻塞,即信号量依然>=0
dispatch_semaphore_signal(semaphore);
});
}
dispatch_once
void
dispatch_once(dispatch_once_t *predicate,
DISPATCH_NOESCAPE dispatch_block_t block);
在程序的生命周期只执行block一次
这个函数对程序中的全局数据(单例)的初始化非常有用。总是在使用或测试任何通过block初始化的变量之前使用这个函数。
如果在多个线程中同时调用,这个函数会同步地等待知道block执行完成。
这个predicate参数必须指向一个保存在全局区或静态区的变量。使用自动存储或动态存储变量(包括OC实例变量)的predicate结果是未知的。
static Singleton *instance;
+ (instancetype)shareInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!instance) {
instance = [[[self class] alloc] init];
}
});
return instance;
}
dispatch_io、dispatch_data
读取较大文件时,如果将文件分成合适的大小并使用Global Queue并行读取的话应该会比一般的读取速度快不少,现今的输入输出硬件已经可以做到一次使用多个线程更快地并列读取了,能实现这一功能的就是dispatch_io和dispatch_data.
使用dispatch_io读写文件可以将1个文件固定大小分为快分别在多个线程read/write.
dispatch_async (queue, ^{/*读取 0 ~ 8191 字节*/});
dispatch_async (queue, ^{/*读取 8192 ~ 16383 字节*/});
dispatch_async (queue, ^{/*读取 16384 ~ 24575 字节*/});
dispatch_async (queue, ^{/*读取 24576 ~ 36767 字节*/});
这里有一个可以分块读取,然后拼装为NSData的方法:
void read_file(int fd, void(^completion)(NSData *data)) {
NSMutableData *data = [NSMutableData data];
dispatch_queue_t pipe_q;
dispatch_io_t pipe_channel;
pipe_q = dispatch_queue_create("PipeQ", NULL);
// pipe_q = dispatch_get_main_queue();
pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, pipe_q, ^(int err){
close(fd);
});
dispatch_io_set_low_water(pipe_channel, SIZE_MAX);
dispatch_io_read(pipe_channel, 0, SIZE_MAX, pipe_q, ^(bool done, dispatch_data_t pipedata, int err){
NSLog(@"%@", [NSThread currentThread]);
if (err == 0) {
size_t len = dispatch_data_get_size(pipedata);
if (len > 0) {
const char *bytes = NULL;
(void)dispatch_data_create_map(pipedata, (const void **)&bytes, &len);
[data appendBytes:bytes length:len];
}
}
if (done && completion) {
completion(data.copy);
}
});
}
使用时,传入文件描述即可:
int fd = open("/Users/Mike/Desktop/a.txt", O_RDWR);
read_file(fd, ^(NSData *data) {
NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
});
dispatch_source
GCD中除了主要的Dispatch Queue外,还有不太引人注目的Dispatch Source。它是BSD系统内核惯有功能kqueue的包装。
kqueue是在XNU内核发生各种事件时,在应用程序编程方执行处理的技术。其CPU负荷非常小,尽量不占用资源,kqueue可以说是应用程序处理XNU内核发生的各种事件的方法中最优秀的一种。
dispatch_source使用流程大致如下:
1.通过dispatch_source_create()
函数创建一个source
2.通过dispatch_source_set_event_handler()
函数为source指定处理的block。
通过dispatch_source_set_cancel_handler()
函数为source指定取消的回调,这个回调会通过dispatch_source_cancel()
函数的调用触发。
3.通过dispatch_resume()
函数启动source
dispatch_source_create
创建一个新的source来管理底层的系统对象,同时自动提交handler block到GCD队列中来响应事件。
GCD source不是可重入的。任何在source暂停时或者在handler block正在执行时接收到的事件都会合并然后在source恢复或者事件handler block返回之后分发。GCD source创建时的状态是挂起的。在创建之后或者设置一些属性(比如handler或者context)之后,你的程序必须调用dispatch_resume来开始这个事件的分发。
如果你的app没有使用ARC,你应该在不再使用source的时候调用dispatch_release来释放它
Important
事件source的创建时异步的,所以要搞清楚被监控的系统句柄产生的竞争条件。比如,如果一个source是为一个进程创建的,同时这个进程在source创建之前就存在了,那么任何设定的取消处理都不会被调用。
参数
type
source的类型。必须是下面列出的source类型常量之一
type | 内容 |
---|---|
DISPATCH_SOURCE_TYPE_DATA_ADD | 变量增加 |
DISPATCH_SOURCE_TYPE_DATA_OR | 变量OR |
DISPATCH_SOURCE_TYPE_MACH_RECV | Mach端口发送 |
DISPATCH_SOURCE_TYPE_MACH_SEND | Mach端口接收 |
DISPATCH_SOURCE_TYPE_PROC | 检测到与进程相关的事件 |
DISPATCH_SOURCE_TYPE_READ | 可读取文件映像 |
DISPATCH_SOURCE_TYPE_SIGNAL | 接收信号 |
DISPATCH_SOURCE_TYPE_TIMER | 定时器 |
DISPATCH_SOURCE_TYPE_VNODE | 文件系统有变更 |
DISPATCH_SOURCE_TYPE_WRITE | 可写入文件映像 |
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE | 内存压力 |
handle
要监控的底层系统句柄。这个参数的具体值依赖于type参数常量。
mask
期望的事件指定的flags的掩码。这个参数的具体值依赖于type参数常量。
queue
事件处理的block提交到的GCD队列。
Returns
创建成功返回新创建的source,否则返回NULL。
dispatch_source_set_event_handler
为指定的source设置事件处理block。
事件处理(如果设置了)会提交到source的目标队列来响应事件的发生。
dispatch_source_set_cancel_handler
为指定的source设置事件取消block。
取消block(如果设置了)会提交到source的目标队列来响应dispatch_source_cancel的调用,这个响应发生在系统释放了所有对source的底层句柄,同时,source事件处理的block已经返回了
Important
为了能安全地关闭文件描述或者销毁Mach port, 取消的处理对文件描述或者port来说是必须的。在取消处理执行之前关闭描述和port会产生竞争条件。当source事件处理仍在运行时,如果有一个新的描述被分配了与最近关闭的描述同样的值,事件处理可能会使用错误的描述读写数据。
dispatch_source_cancel
异步地取消source,阻止对事件处理block的再次调用。
取消操作阻止任何对事件处理block的再次调用,但是不会打断正在执行的事件处理。一旦事件处理block执行完毕,这个可选的取消处理会提交到目标队列。(事件处理至少执行一次??)
取消操作会在时间处理执行完成时提交到目标队列,意味着关闭source的句柄(文件描述或者mach port)是安全的。
这个可选的取消处理只会在系统释放了所有的对底层系统对象(文件描述或mach prots)的引用之后才提交到目标队列。因此,取消处理是一个关闭和释放这些系统对象非常方便的地方。要注意的是,如果文件描述或mach port最近被被source对象追踪,在取消操作执行之前关闭或者释放它们是无效的.
dispatch_source_set_timer
为一个timer source设置开始时间,间隔,偏差。
你的程序可以根据需要在一个timer source对象上多次调用这个函数来重置时间间隔。
开始时间这个参数也决定了这个timer上将使用什么时钟。如果开始时间是DISPATCH_TIME_NOW或者是dispatch_time函数创建的,这个timer基于mach_absolute_time。否则,timer的开始时间是dispatch_walltime创建的,这个timer基于gettimeofday(3)。
偏差参数是程序时间值的微量,它以毫秒为单位,为了提升系统性能和解决耗电量,它可以大到系统将timer延迟到与其他系统活动结合使用。例如,一个每5分钟执行一个周期性任务,而程序可能会使用一个最多30秒的leeway值。要注意的是:对所有的timer而言,即使leeway值指定为0,一些潜在问题也是正常的。
调用这个函数不会对已经取消的timer source有作用。
参数
start
timer开始的时间。查看 dispatch_time 和 dispatch_walltime 获取更多信息。
interval
以毫秒为单位的时间间隔
leeway
系统可以延迟这个timer的时间值,以毫秒为单位。
dispatch_source_testcancel
测试指定的source是否已经被取消。
你的程序会使用这个函数来测试GCD source对象是否已经被通过调用dispatch_source_cancel的方式取消。如果dispatch_source_cancel已经调用过了,这个函数会立刻返回一个非0值,如果没有被取消则返回0.
一些其他方法
dispatch_source_get_data
返回数据。
要在事件处理的block中调用这个函数。在外面调用会发生意想不到的结果。
返回值(unsigned long )
根据source的type的不同会有不同的返回值,共有以下几种:
type | 返回值 |
---|---|
DISPATCH_SOURCE_TYPE_DATA_ADD | 程序定义的数据 |
DISPATCH_SOURCE_TYPE_DATA_OR | 程序定义的数据 |
DISPATCH_SOURCE_TYPE_MACH_SEND | Dispatch Source Mach Send Event Flags |
DISPATCH_SOURCE_TYPE_MACH_RECV | 不适用 |
DISPATCH_SOURCE_TYPE_PROC | Dispatch Source Process Event Flags |
DISPATCH_SOURCE_TYPE_READ | 预估可读字节数 |
DISPATCH_SOURCE_TYPE_SIGNAL | 上次处理执行后的分发的signal数目 |
DISPATCH_SOURCE_TYPE_TIMER | 上次处理执行之后,timer启动后执行的次数 |
DISPATCH_SOURCE_TYPE_VNODE | Dispatch Source Vnode Event Flags |
Dispatch Source Memory Pressure Event Flags | 可用的预估缓存空间 |
dispatch_source_get_mask
返回source监控的事件的掩码。
这个掩码是一个事件source监控的相关事件的位掩码。任何在这个事件掩码里没有指定的事件都胡被忽略,同时不会为这些事件提交事件处理block。
更详细的信息 查看flag描述常量。
返回值
返回值根据source的type不同,会是以下flag集合中的一种:
type | 返回值 |
---|---|
DISPATCH_SOURCE_TYPE_MACH_SEND | Dispatch Source Mach Send Event Flags |
DISPATCH_SOURCE_TYPE_PROC | Dispatch Source Process Event Flags |
DISPATCH_SOURCE_TYPE_VNODE | Dispatch Source Vnode Event Flags |
dispatch_source_get_handle
返回与指定source关联的底层系统句柄。
这个返回的句柄是一个对source监控的底层系统对象的引用。
返回值
这个返回值根据source的类型不同而不同,它回事以下句柄中的一种:
| DISPATCH_SOURCE_TYPE_MACH_SEND | mach port (mach_port_t) |
| DISPATCH_SOURCE_TYPE_MACH_RECV | mach port (mach_port_t) |
| DISPATCH_SOURCE_TYPE_PROC | process identifier (pid_t) |
| DISPATCH_SOURCE_TYPE_READ | file descriptor (int) |
| DISPATCH_SOURCE_TYPE_SIGNAL | signal number (int) |
| DISPATCH_SOURCE_TYPE_VNODE | file descriptor (int) |
| Dispatch Source Memory Pressure Event Flags | file descriptor (int) |
dispatch_source_merge_data
合并数据到GCD source,这个source的类型为DISPATCH_SOURCE_TYPE_DATA_ADD
或者DISPATCH_SOURCE_TYPE_DATA_OR
,然后将事件处理提交到目标队列。
你的程序使用这个函数来处理DISPATCH_SOURCE_TYPE_DATA_ADD
类型或DISPATCH_SOURCE_TYPE_DATA_OR
类型的事件
参数
value
使用逻辑或、逻辑与组合的source类型。传0没有任何作用,也不会提交处理block。
dispatch_get_current_queue为什么被废弃
有两个串行队列
dispatch_queue_t queueA = dispatch_queue_create("com.mikezh.queueA", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queueB = dispatch_queue_create("com.mikezh.queueB", DISPATCH_QUEUE_SERIAL);
下面会发生死锁:
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
//do something
};
if (dispatch_get_current_queue() == queueA) {
block();
}else{
dispatch_sync(queueA, block); // 程序崩溃在这里
}
});
});
为什么会这里会发生死锁:
首先这里的队列嵌套关系如下所示:
我们使用
if (dispatch_get_current_queue() == queueA)
进行判断的目的是为了防止在当前队列是queueA的情况下,向queueA中添加同步执行的block,因为这样会发生死锁。但是队列嵌套关系表明,当前所在的队列queueB被queueA嵌套,但是糟糕的是:dispatch_get_current_queue函数只能返回最内层的队列queueB,所以这个判断的结果不能让我们完成起初的目的了。
这里已经充分说明了dispatch_get_current_queue的弊端:我们想获知block执行的环境是否被某个队列嵌套来避免死锁,而它只是简单地只拿到最内层的队列的功能无法解决这个问题。
在说一些解决的方法之前,看一个上面一个功能的等价写法(如果不理解可以查看dispatch_set_target_queue部分):
dispatch_set_target_queue(queueB, queueA);
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
//do something
};
if (dispatch_get_current_queue() == queueA) {
block();
}else{
dispatch_sync(queueA, block); // 程序崩溃在这里
}
});
依然是死锁的。
下面我们来介绍一种但是如果使用specific来判断当前的队列,就不会死锁:
dispatch_set_target_queue(queueB, queueA);
static int specificKey;
CFStringRef specificValue = CFSTR("at hierarchy under queueA");
dispatch_queue_set_specific(queueA,
&specificKey,
(void*)specificValue,
(dispatch_function_t)CFRelease);
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
//do something
};
CFStringRef retrievedValue = dispatch_get_specific(&specificKey);
if (retrievedValue == specificValue) {
block(); // 程序走的这条分支,而不是下面的
} else {
dispatch_sync(queueA, block);
}
});
我们可以通过dispatch_get_specific函数准确得知在队列层级关系中是否存在specificKey指定的值,也就是说加入根据根据指定的键获取不到关联数据,那么系统会沿着层级体系向上查找,知道找到数据或到达根队列位置。这里要指出的是,层级里地位最高的那个队列总是全局并发队列。这个过程如下图所示:
所以上面的代码执行到dispatch_get_specific时会在queueA找到之前设定好的值,然后返回。这时队列的层级关系中得知存在queueA队列,直接调用block()
,而不再走下面的分支dispatch_sync(queueA, block);
,从而有效避免了死锁。