哇。好的——我最初的绩效评估完全错误。给我涂上愚蠢的颜色。
没那么傻。我的性能测试是错误的。固定的。同时深入研究 GCD 代码。
更新:可以在此处找到基准代码:https://github.com/bbum/StackOverflow 希望它现在是正确的。 :)
Update2:为每种测试添加了一个 10 队列版本。
好的。重写答案:
• @synchronized() 已经存在很长时间了。它被实现为哈希查找以查找然后锁定的锁。它“相当快”——通常足够快——但在高争用情况下可能会成为负担(任何同步原语都可能如此)。
• dispatch_sync() 不一定需要锁,也不需要复制块。具体来说,在 fastpath 的情况下,dispatch_sync() 将直接在调用线程上调用块,而不复制块。即使在 slowpath 的情况下,块也不会被复制,因为调用线程必须阻塞直到执行(调用线程被挂起,直到 dispatch_sync() 之前的任何工作完成,然后线程恢复)。一个例外是在主队列/线程上调用;在这种情况下,该块仍然没有被复制(因为调用线程被挂起,因此,使用堆栈中的一个块是可以的),但是有很多工作完成了在主队列中排队、执行和然后恢复调用线程。
• dispatch_async() 要求复制该块,因为它不能在当前线程上执行也不当前线程不能被阻塞(因为该块可能会立即锁定一些线程本地资源,仅在 dispatch_async() 之后的代码行中可用。虽然成本高昂,dispatch_async() 将工作从当前线程移开,使其可以立即恢复执行。
最终结果 -- dispatch_sync() 比 @synchronized 快,但不是一个普遍有意义的数量(在 '12 iMac 和 '11 mac mini 上 -- 两者之间的#s 非常不同,顺便说一句.. .并发的乐趣)。使用dispatch_async() 比在无争议的情况下都慢,但不会慢很多。但是,当资源处于争用状态时,使用“dispatch_async()”会明显更快。
@synchronized uncontended add: 0.14305 seconds
Dispatch sync uncontended add: 0.09004 seconds
Dispatch async uncontended add: 0.32859 seconds
Dispatch async uncontended add completion: 0.40837 seconds
Synchronized, 2 queue: 2.81083 seconds
Dispatch sync, 2 queue: 2.50734 seconds
Dispatch async, 2 queue: 0.20075 seconds
Dispatch async 2 queue add completion: 0.37383 seconds
Synchronized, 10 queue: 3.67834 seconds
Dispatch sync, 10 queue: 3.66290 seconds
Dispatch async, 2 queue: 0.19761 seconds
Dispatch async 10 queue add completion: 0.42905 seconds
对以上内容持保留态度;它是最差的一种微型基准,因为它不代表任何现实世界的常见使用模式。 “工作单元”如下,上面的执行次数代表1,000,000次执行。
- (void) synchronizedAdd:(NSObject*)anObject
{
@synchronized(self) {
[_a addObject:anObject];
[_a removeLastObject];
_c++;
}
}
- (void) dispatchSyncAdd:(NSObject*)anObject
{
dispatch_sync(_q, ^{
[_a addObject:anObject];
[_a removeLastObject];
_c++;
});
}
- (void) dispatchASyncAdd:(NSObject*)anObject
{
dispatch_async(_q, ^{
[_a addObject:anObject];
[_a removeLastObject];
_c++;
});
}
(_c 在每次传递开始时重置为 0,并在结束时断言为 == 到 # 个测试用例,以确保代码在喷出时间之前实际上正在执行所有工作。)
对于无争议的情况:
start = [NSDate timeIntervalSinceReferenceDate];
_c = 0;
for(int i = 0; i < TESTCASES; i++ ) {
[self synchronizedAdd:o];
}
end = [NSDate timeIntervalSinceReferenceDate];
assert(_c == TESTCASES);
NSLog(@"@synchronized uncontended add: %2.5f seconds", end - start);
对于竞争的,2个队列,case(q1和q2是串行的):
#define TESTCASE_SPLIT_IN_2 (TESTCASES/2)
start = [NSDate timeIntervalSinceReferenceDate];
_c = 0;
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
dispatch_apply(TESTCASE_SPLIT_IN_2, serial1, ^(size_t i){
[self synchronizedAdd:o];
});
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
dispatch_apply(TESTCASE_SPLIT_IN_2, serial2, ^(size_t i){
[self synchronizedAdd:o];
});
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
end = [NSDate timeIntervalSinceReferenceDate];
assert(_c == TESTCASES);
NSLog(@"Synchronized, 2 queue: %2.5f seconds", end - start);
以上内容只是针对每个工作单元变体重复(没有使用棘手的运行时魔法;copypasta FTW!)。
考虑到这一点:
• 如果您喜欢它的外观,请使用@synchronized()。现实情况是,如果您的代码在该数组上竞争,您可能遇到了架构问题。 注意:使用@synchronized(someObject) 可能会产生意想不到的后果,如果对象内部使用@synchronized(self),它可能会导致额外的争用!
• 如果您喜欢,请使用带有串行队列的dispatch_sync()。没有开销——在竞争和非竞争的情况下它实际上都更快——并且使用队列更容易调试和更容易分析,因为 Instruments 和 Debugger 都有用于调试队列的优秀工具(而且它们正在变得更好一直),而调试锁可能会很痛苦。
• 将dispatch_async() 与不可变数据一起用于竞争激烈的资源。即:
- (void) addThing:(NSString*)thing {
thing = [thing copy];
dispatch_async(_myQueue, ^{
[_myArray addObject:thing];
});
}
最后,用哪一个来维护数组的内容并不重要。同步案例的争用成本非常高。对于异步情况,争用成本会大大降低,但复杂性或奇怪性能问题的可能性会大大增加。
在设计并发系统时,最好保持队列之间的边界尽可能小。其中很大一部分是确保尽可能少的资源在边界两侧“存活”。