在介绍完GCD的基本使用后,接下来聊聊GCD的一些高级功能。
dispatch_semaphore(信号量)
信号量是用来管理对资源的并发访问。信号量是持有计数的信号,内部有一个可以原子递增或递减的值。如果有一个操作尝试减少信号量的值,使其小于0,那么该操作将会被阻塞(或等待),直到有其它操作增加该信号量的值(>=1时)。
首先,我们来看下如下这种情况。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; i++) {
dispatch_async(queue, ^{
[array addObject:[[NSObject alloc] init]];
});
}
在不考虑顺序的情况下,将所有数据追加到NSMutableArray中。因为在全局队列中异步更新NSMutableArray对象,执行后程序会因为内存错误而崩溃。
使用dispatch_semaphore对代码进行改造,改造后的代码如下:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 初始化信号量计数为1,保证可同时访问NSMutableArray对象的线程只有1个
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100000; i++) {
dispatch_async(queue, ^{
// 一直等待,直到信号量的计数>=1
// 信号量计数-1,dispatch_semaphore_wait函数返回
// 此时信号量计数为0,由于可同时访问NSMutableArray对象的线程只有1个
// (其它线程此时无法访问NSMutableArray对象),因此可以安全的进行更新
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[array addObject:[[NSObject alloc] init]];
// 处理结束,信号量计数+1,此时其它线程可以访问NSMutableArray对象
dispatch_semaphore_signal(semaphore);
});
}
-
信号量在实际开发中使用
-
将异步操作转换为同步操作
例如:在做请求接口的单元测试时,需要等待响应回调。可以在调用接口后等待信号量,然后在回调里通知该信号量。
NSURL *URL = [NSURL URLWithString:@"http://xxx.com"]; __block NSURL *location; __block NSError *error; // 创建信号量并初始化为0(等待响应结果,阻塞) dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [[[NSURLSession sharedSession] downloadTaskWithURL:URL completionHandler: ^(NSURL *l, NSURLResponse *r, NSError *e) { location = l; error = e; // 响应处理结束,信号量计数+1,解除阻塞 dispatch_semaphore_signal(semaphore); }] resume]; // 设置等待超时时间 double timeoutInSeconds = 2.0; dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutInSeconds * NSEC_PER_SEC)); long timeoutResult = dispatch_semaphore_wait(semaphore, timeout); // 断言测试 XCTAssertEqual(timeoutResult, 0L, @"Timed out"); XCTAssertNil(error, @"Received an error:%@", error); XCTAssertNotNil(location, @"Did not get a location");
-
YYModel中信号量的使用
``` NSObject+YYModel.m + (instancetype)metaWithClass:(Class)cls { if (!cls) return nil; static CFMutableDictionaryRef cache; static dispatch_once_t onceToken; static dispatch_semaphore_t lock; dispatch_once(&onceToken, ^{ cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); lock = dispatch_semaphore_create(1); }); dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); _YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls)); dispatch_semaphore_signal(lock); if (!meta || meta->_classInfo.needUpdate) { meta = [[_YYModelMeta alloc] initWithClass:cls]; if (meta) { dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); CFDictionarySetValue(cache, (__bridge const void *)(cls), (__bridge const void *)(meta)); dispatch_semaphore_signal(lock); } } return meta; } ```
-
-
使用信号量注意事项
- 信号量不依赖GCD调度队列,它可以直接在任何线程中使用
- 信号量属于底层工具,应该优先考虑使用诸如操作队列这样的高级API
- 信号量本身是锁,能不用就不用
dispatch_barrier_async
在日常开发中我们经常会遇到这样的情景,需要并发的对数据库进行读、写操作。在多线程环境下并发读、写数据库特别容易产生死锁及其它错误(如:脏读等问题)。
dispatch_queue_t queue = dispatch_queue_create("com.test.gcd.barrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// do some read task
NSLog(@"read0 task");
});
dispatch_async(queue, ^{
// do some read task
NSLog(@"read1 task");
});
dispatch_async(queue, ^{
// do some read task
NSLog(@"read2 task");
});
dispatch_async(queue, ^{
// do some read task
NSLog(@"read3 task");
});
dispatch_async(queue, ^{
// do some read task
NSLog(@"read4 task");
});
dispatch_async(queue, ^{
// do some read task
NSLog(@"read5 task");
});
如上所示,在并发读时不会产生死锁等问题。假如:在read2 task之后需要增加一个写入操作,并将其添加到并发队列中,read3以及之后的读取操作需要读取写入的值。
dispatch_queue_t queue = dispatch_queue_create("com.test.gcd.barrier", DISPATCH_QUEUE_CONCURRENT);
...
dispatch_async(queue, ^{
// do some read task
NSLog(@"read2 task");
});
dispatch_async(queue, ^{
// do some write task
NSLog(@"write task");
});
dispatch_async(queue, ^{
// do some read task
NSLog(@"read3 task");
});
...
由并发队列的特性可知,以上代码并不会达到预期目的。如果写入操作增多还会导致数据库资源竞争产生死锁,甚至导致应用异常结束。使用dispatch_barrier_async可以帮我们避免此类问题。
dispatch_queue_t queue = dispatch_queue_create("com.test.gcd.barrier", DISPATCH_QUEUE_CONCURRENT);
...
dispatch_async(queue, ^{
// do some read task
NSLog(@"read2 task");
});
dispatch_barrier_async(queue, ^{
// do some write task
NSLog(@"write task");
});
dispatch_async(queue, ^{
// do some read task
NSLog(@"read3 task");
});
...
dispatch_barrier_async会等到追加到并发队列上的read0read2任务处理结束后,再将write任务追加到并发队列中,write任务处理结束后,追加后续read3read5任务以并发方式执行。执行结果如下所示。
2017-xx-xx GCDAdvanceDemo[45482:1778050] read2 task
2017-xx-xx GCDAdvanceDemo[45482:1778038] read0 task
2017-xx-xx GCDAdvanceDemo[45482:1778039] read1 task
2017-xx-xx GCDAdvanceDemo[45482:1778041] write task
2017-xx-xx GCDAdvanceDemo[45482:1778041] read3 task
2017-xx-xx GCDAdvanceDemo[45482:1778039] read4 task
2017-xx-xx GCDAdvanceDemo[45482:1778050] read5 task
dispatch_barrier_async在并发队列中执行流程如下图所示。
dispatch_barrier_async.png
dispatch_source_t(事件源)
事件源可以用来响应并处理系统事件,如:监视进程、文件的变化情况。由于iOS系统的限制,该功能主要用于Mac开发中。
- 在iOS中一些性能要求较高的场合可以使用
自定义源
来进行处理进度的反馈,如下所示。
// 自定义事件源,指定累积方式和事件处理队列
dispatch_source_t
source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD,
0, 0, dispatch_get_main_queue());
// 事件处理,同一时间只有一个处理块被分派,如果该块尚未处理完成另一事件已发生,
// 事件会以指定的累积方式DISPATCH_SOURCE_TYPE_DATA_ADD进行累积
__block long totalComplete = 0;
dispatch_source_set_event_handler(source, ^{
// 获取数据,处理后清空
long value = dispatch_source_get_data(source);
totalComplete += value;
self.progressView.progress = (CGFloat)totalComplete/100.0f;
});
// 事件源默认处于暂停状态,需要手动恢复
dispatch_resume(source);
dispatch_queue_t
queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0);
dispatch_async(queue, ^{
for (int i = 0; i <= 100; ++i) {
// 向事件源发送数据,数据需要>0否则不会触发事件
dispatch_source_merge_data(source, 1);
usleep(20000);
}
});
- 使用DISPATCH_SOURCE_TYPE_TIMER事件源实现定时器
+ (RNTimer *)repeatingTimerWithTimeInterval:(NSTimeInterval)seconds
block:(void (^)(void))block {
RNTimer *timer = [[self alloc] init];
timer.block = block;
// 指定事件源类型
timer.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0,
dispatch_get_main_queue());
// 定时器参数设置
uint64_t nsec = (uint64_t)(seconds * NSEC_PER_SEC);
dispatch_source_set_timer(timer.source,
dispatch_time(DISPATCH_TIME_NOW, nsec),
nsec, 0);
// 事件处理block设置
dispatch_source_set_event_handler(timer.source, block);
dispatch_resume(timer.source);
return timer;
}
完整实现请移步RNTimer
dispatch_data、dispatch_io(派发数据、派发IO)
在开发中处理大量IO操作是一件比较有挑战的事情,比如:读取大文件。一种常见的思路是,将文件分割成合适的大小并发读取,如下所示。
dispatch_async(queue, ^{ /* 读取 0 ~ 8080 字节*/ });
dispatch_async(queue, ^{ /* 读取 8081 ~ 16383 字节*/ });
dispatch_async(queue, ^{ /* 读取 16384 ~ 24575 字节*/ });
...
使用dispatch_data和dispatch_io可以达到上述目的,以下为Apple System Log API的部分代码,展示了如何使用dispatch_data、dispatch_io
pipe_q = dispatch_queue_create("PipeQ", NULL);
// 创建 Dispatch I/O
pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, pipe_q, ^(int err){
close(fd);
});
*out_fd = fdpair[1];
// 设定单次读取数据的大小
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){
if (err == 0)
{
// 获取“单个文件块”的大小
size_t len = dispatch_data_get_size(pipedata);
if (len > 0)
{
// 定义一个字节数组bytes
const charchar *bytes = NULL;
charchar *encoded;
// 数据处理
dispatch_data_t md = dispatch_data_create_map(pipedata, (const voidvoid **)&bytes, &len);
encoded = asl_core_encode_buffer(bytes, len);
asl_set((aslmsg)merged_msg, ASL_KEY_AUX_DATA, encoded);
free(encoded);
_asl_send_message(NULL, merged_msg, -1, NULL);
asl_msg_release(merged_msg);
dispatch_release(md);
}
}
if (done)
{
dispatch_semaphore_signal(sem);
dispatch_release(pipe_channel);
dispatch_release(pipe_q);
}
});
DispatchDownload展示了如何使用dispatch_io建立和server端的socket通信并通过流的方式下载文件。
dispatch_io属于底层C语言API,在使用时应该优先考虑使用高级API,在高级API无法满足需求的情况下,可以考虑使用底层API以获取更多的控制权。
参考
底层并发API
RNTimer
iOS 7 Programming Pushing the Limits
Objective-C高级编程
网友评论