dispatch_sync相对于@synchronized有什么优势?

33

假设我想要将这段代码变得线程安全:

- (void) addThing:(id)thing { // Can be called from different threads
    [_myArray addObject:thing];
}

GCD 似乎是实现这一目标的首选方式:

- (void) addThing:(id)thing { 
    dispatch_sync(_myQueue, ^{  // _myQueue is serial.
        [_myArray addObject:thing];
    });    
}

它相对传统方法有哪些优势?

- (void) addThing:(id)thing {
    @synchronized(_myArray) {
        [_myArray addObject:thing];
    }
}

“GCD似乎是首选的方式”...您有关于该声明的任何参考资料吗? - Martin R
@MartinR 我从GCD WWDCs中感觉到这是现代的方式。不过我可能错了。 - Robert
大多数创新都是更好的...但并非总是如此!!! - Anoop Vaidya
3
好的,我想我找到了一个参考文献。@synchronised在底层使用了锁对吧?“队列不会像锁一样带来相同的惩罚。例如,将任务排队不需要陷入内核以获取互斥锁。”http://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/ThreadMigration/ThreadMigration.html#//apple_ref/doc/uid/TP40008091-CH105-SW3 - Robert
它们难道不是完全不同的策略吗?在同一队列的递归调用中,您不能使用dispatch_sync,否则会死锁。 - Tom Andersen
4个回答

53

哇。好的——我最初的性能评估完全错了。把我涂成愚蠢的颜色。

不太愚蠢。我的性能测试有问题。已经修复了。还进行了深入的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];
    });    
}

最后,使用哪种方法维护数组的内容并不重要。同步情况下争用成本极高,而异步情况下争用成本大大降低,但是可能会带来复杂性或奇怪的性能问题。

在设计并发系统时,最好将队列之间的边界保持尽可能小。其中一个重要方面是确保尽可能少的资源“生活”在边界的两侧。



不错的回答 - 但我并没有真正理解增加灵活性的论点 - 在@synchronized方式中,你也不能这样做thing = [thing copy];吗? - Robert
其实我也不太理解 'dispatch_sync()' 的快速路径是什么?据我所知,调度队列是线程的包装器。在什么情况下会出现快速路径,为什么不使用线程?你有参考资料吗? - Robert
1
@Robert:切换线程是很耗费资源的,而且如果不必要的话,dispatch_sync()也不会使用另一个线程:https://gist.github.com/woolsweater/5882484。我不完全知道“不必要”包含什么,但是如果你有兴趣的话,[libdispatch是开源的](http://opensource.apple.com/source/libdispatch/libdispatch-228.23/)。在我的小演示中,无论如何原始线程都必须等待操作结果,因此切换似乎是不必要的。 - jscs
2
@bbum 在你的方法 addThing: 中,你使用了 dispatch_async -- 而不是 dispatch_sync。这是意外吗?另一个后续问题:假设同步队列没有过多争用,实际执行的代码非常少(基本上只是一个赋值操作)。在这种情况下,你会建议使用 dispatch_sync 而不是 dispatch_async 吗 -- 鉴于同步可能会导致死锁的额外危险可以被管理? - CouchDeveloper
@CouchDeveloper 相当有意义。请阅读周围的文本。 - bbum
显示剩余2条评论

2

我发现dispatch_sync()是一种糟糕的锁定方式,它不支持嵌套调用。

因此,您不能在串行Q上调用dispatch_sync(),然后在具有相同Q的子例程中再次调用它。这意味着它根本不像@synchronized的行为。


你需要编写自己的dispatchOnSerialThread方法,该方法检查当前线程是否等于SerialThread,如果是,则在同一线程上执行块;如果不是,则在SerialThread上执行dispatch_sync()。 - Hamidreza Vakilian
它也不支持返回并且你必须执行__block舞蹈。 - malhal

1

好的,我做了更多的测试,以下是结果:

锁测试:平均值:2.48661,标准差:0.50599

同步测试:平均值:2.51298,标准差:0.49814

调度测试:平均值:2.17046,标准差:0.43199

所以我错了,我的错 :( 如果有人对测试代码感兴趣,可以在这里找到:

static NSInteger retCount = 0;

@interface testObj : NSObject
@end

@implementation testObj

-(id)retain{
    retCount++;
    return [super retain];
}
@end

@interface ViewController : UIViewController{
    NSMutableArray* _a;
    NSInteger _c;
    NSLock* lock;
    NSLock* thlock;
    dispatch_queue_t _q;
}

- (IBAction)testBtn:(id)sender;
@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
}

-(NSTimeInterval)testCase:(SEL)aSel name:(NSString*)name{
    _a = [[NSMutableArray alloc] init];
    retCount = 0;
    //Sync test
    NSThread* th[10];
    for(int t = 0; t < 10;t ++){
        th[t] = [[NSThread alloc] initWithTarget:self selector:aSel object:nil];
    }

    NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
    for(int t = 0; t < 10;t ++){
        [th[t] start];
    }
    NSInteger thCount = 1;
    while(thCount > 0){
        thCount = 0;
        for(int t = 0; t < 10;t ++){
            thCount += [th[t] isFinished] ? 0 : 1;
        }
    }
    NSTimeInterval end = [NSDate timeIntervalSinceReferenceDate];
    NSLog(@"%@: %2.5f, retainCount:%d, _c:%d, objects:%d", name, end-start, retCount, _c, [_a count]);
    [_a release];
    for(int t = 0; t < 10;t ++){
        [th[t] release];
    }
    return end-start;
}

-(void)syncTest{
    for(int t = 0; t < 5000; t ++){
        [self synchronizedAdd:[[[testObj alloc] init] autorelease] ];
    }
}

-(void)dispTest{
    for(int t = 0; t < 5000; t ++){
        [self dispatchSyncAdd:[[[testObj alloc] init] autorelease] ];
    }
}

-(void)lockTest{
    for(int t = 0; t < 5000; t ++){
        [self lockAdd:[[[testObj alloc] init] autorelease] ];
    }
}


- (void) synchronizedAdd:(NSObject*)anObject
{
    @synchronized(self) {
        [_a addObject:anObject];
        _c++;
    }
}

- (void) dispatchSyncAdd:(NSObject*)anObject
{
    dispatch_sync(_q, ^{
        [_a addObject:anObject];
        _c++;
    });
}

- (void) lockAdd:(NSObject*)anObject
{
    [lock lock];
        [_a addObject:anObject];
        _c++;
    [lock unlock];
}

- (double)meanOf:(NSArray *)array
{
    double runningTotal = 0.0;

    for(NSNumber *number in array)
    {
        runningTotal += [number doubleValue];
    }

    return (runningTotal / [array count]);
}

- (double)standardDeviationOf:(NSArray *)array
{
    if(![array count]) return 0;

    double mean = [self meanOf:array];
    double sumOfSquaredDifferences = 0.0;

    for(NSNumber *number in array)
    {
        double valueOfNumber = [number doubleValue];
        double difference = valueOfNumber - mean;
        sumOfSquaredDifferences += difference * difference;
    }

    return sqrt(sumOfSquaredDifferences / [array count]);
}

-(void)stats:(NSArray*)data name:(NSString*)name{
    NSLog(@"%@: mean:%2.5f, stdDev:%2.5f", name, [self meanOf:data], [self standardDeviationOf:data]);
}

- (IBAction)testBtn:(id)sender {
    _q = dispatch_queue_create("array q", DISPATCH_QUEUE_SERIAL);
    lock = [[NSLock alloc] init];
    NSMutableArray* ltd = [NSMutableArray array];
    NSMutableArray* std = [NSMutableArray array];
    NSMutableArray* dtd = [NSMutableArray array];
    for(int t = 0; t < 20; t++){
        [ltd addObject: @( [self testCase:@selector(lockTest) name:@"lock Test"] )];
        [std addObject: @( [self testCase:@selector(syncTest) name:@"synchronized Test"] )];
        [dtd addObject: @( [self testCase:@selector(dispTest) name:@"dispatch Test"] )];
    }
    [self stats: ltd name:@"lock test"];
    [self stats: std name:@"synchronized test"];
    [self stats: dtd name:@"dispatch Test"];
}
@end

-1

有几件事情: 1)@Synchronize 是某些监视器上锁定的重量级版本(我个人更喜欢NSLock/NSRecursiveLock) 2)Dispatch_sync 构建执行队列。

在您的情况下,这两种方法都会导致类似的结果,但对于如此简单的解决方案,使集合线程安全,我更喜欢第一种方法。

为什么:

  • 如果您有多个核心,则多个线程可以同时工作。根据调度程序,它们将在监视器上短时间锁定。

  • 它比分配新块、保留“thing”并将其放入队列(这也是线程同步的)以及在工作队列准备好时执行轻得多。

  • 在这两种方法中,执行顺序将非常不同。

  • 如果在某个时刻发现集合的使用很重,则可以考虑将锁更改为读/写类型,如果使用某个类似于 NSLock 的类而不是 sync_queue,则更容易进行重构/更改。


如果您将任务添加到同步队列中,则需要对其进行锁定。因此,您无法从锁中运行。获取单个锁比获取锁并管理块(队列)更轻便。将任务放入队列涉及块的创建/处理。在问题场景中,队列比锁更重。 - The Tosters
2
好的--是的,这个答案是不正确的。对于同步调度,没有锁定、分配和保留所需。而且,一般来说,调度队列比锁要轻得多,正是因为内部实现通常根本不使用锁定;而是更喜欢更高效的原子比较和交换。在没有争用的情况下,使用dispatch_sync()处理这个简单的情况会快30%。 - bbum
上面的示例很不错,但这是单线程代码。有空时(可能是周一),我会尝试多线程方法。从上面的示例中来看,简单循环看起来不错,但我很好奇在多线程情况下会怎样运行。虽然我仍然不同意你的看法,但在进行更多测试之后我会做出最终结论。 - The Tosters
我发布的示例既可以是单队列(单线程)也可以是多队列(多线程)。在所有情况下,dispatch_sync速度更快。 - bbum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接