哇。好的——我最初的性能评估完全错了。把我涂成愚蠢的颜色。
不太愚蠢。我的性能测试有问题。已经修复了。还进行了深入的GCD代码研究。
更新:基准测试的代码可以在这里找到:https://github.com/bbum/StackOverflow希望现在是正确的:)
更新2:每种测试都添加了10个队列版本。
好的。正在重新写答案:
• @synchronized()
已经存在很长时间了。它实现为哈希查找以找到一个锁,然后对其加锁。它“相当快”--通常足够快--但在高争用率下可能会成为负担(就像任何同步原语一样)。
• dispatch_sync()
不一定需要锁,也不需要复制块。特别是在快速路径情况下,dispatch_sync()
将直接在调用线程上调用块,而不会复制块。即使在慢路径情况下,由于调用线程必须阻塞,直到执行完成(调用线程被挂起,直到dispatch_sync()
之前的任何工作都完成,然后线程被恢复)。唯一的例外是在主队列/线程上调用;在这种情况下,块仍未复制(因为调用线程被挂起,因此从堆栈中使用块是可以的),但是有大量工作要在主队列上排队执行,然后恢复调用线程。
• dispatch_async()
要求复制块,因为它不能在当前线程上执行也不能阻塞当前线程(因为块可能会立即锁定某些线程本地资源,该资源只在dispatch_async()
之后的代码行上可用)。虽然昂贵,但 dispatch_async()
将工作移出当前线程,使其能够立即恢复执行。
最终结果--dispatch_sync()
比@synchronized
快,但不是一个普遍有意义的数量级(在'12 iMac和'11 Mac mini上,两者之间的数字非常不同,顺便说一句...并发的乐趣)。在未竞争的情况下,使用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
请将上述内容视为参考;这是一种最糟糕的微基准测试,因为它不代表任何真实世界中常见的使用模式。"工作单位"如下,上面的执行时间表示100万次执行。- (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队列情况(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);
针对每个工作单元变量简单地重复上述步骤(没有运行时魔法技巧;复制黏贴万岁!)。
考虑到这一点:
• 如果您喜欢它的外观,请使用 @synchronized()
。实际上,如果您的代码在该数组上竞争,那么您可能存在架构问题。注意:如果对象内部使用@synchronized(self)
,则使用@synchronized(someObject)
可能会导致额外的竞争!
• 如果这是您的喜好,请使用串行队列的 dispatch_sync()
。它没有开销 -- 在有和没有竞争的情况下速度都更快 -- 并且使用队列更容易调试和分析,因为Instruments和调试器都具有出色的调试队列的工具(而且它们越来越好),而调试锁可能很麻烦。
• 对于高度竞争的资源,请使用不可变数据的 dispatch_async()
。即:
- (void) addThing:(NSString*)thing {
thing = [thing copy];
dispatch_async(_myQueue, ^{
[_myArray addObject:thing];
});
}
最后,使用哪种方法维护数组的内容并不重要。同步情况下争用成本极高,而异步情况下争用成本大大降低,但是可能会带来复杂性或奇怪的性能问题。
在设计并发系统时,最好将队列之间的边界保持尽可能小。其中一个重要方面是确保尽可能少的资源“生活”在边界的两侧。