dispatch_sync比@synchronized有什么优势?

时间:2013-07-11 17:06:33

标签: ios objective-c multithreading thread-safety

让我们说我想让这段代码线程安全:

- (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];
    }
}

4 个答案:

答案 0 :(得分:48)

<击>哇。好的 - 我原来的绩效评估是错误的。让我变得愚蠢。

不是那么愚蠢。我的表现测试错了。固定。随着深入研究GCD代码。

更新:基准的代码可以在这里找到:https://github.com/bbum/StackOverflow希望现在是正确的。 :)

Update2:为每种测试添加了10个队列版本。

行。重写答案:

@synchronized()已存在很长时间了。它被实现为哈希查找以查找随后被锁定的锁。它“非常快” - 通常足够快 - 但在高争用下可能是一种负担(任何同步原语也是如此)。

dispatch_sync()不一定需要锁定,也不需要复制块。具体来说,在快速路径的情况下,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队列,案例(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()与串行队列一起使用。没有任何开销 - 在竞争和非竞争情况下实际上都更快 - 并且使用队列更容易调试并且更容易在该工具中进行分析,并且调试器都具有用于调试队列的优秀工具(并且它们正在变得更好所有的时间)而调试锁可能是一种痛苦。

•将dispatch_async()与不可变数据一起用于争用大量资源。即:

- (void) addThing:(NSString*)thing { 
    thing = [thing copy];
    dispatch_async(_myQueue, ^{
        [_myArray addObject:thing];
    });    
}

最后,使用哪一个来维护数组的内容并不重要。同步案例的争用成本非常高。对于异步情况,争用成本会下降,可能会出现复杂性或奇怪的性能问题。

在设计并发系统时,最好保持队列之间的边界尽可能小。其中很大一部分是确保尽可能少的资源“生活”在边界的两边。

答案 1 :(得分:2)

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

因此,您无法在串行Q上调用dispatch_sync,然后在具有相同Q的子例程中再次调用它。这意味着它的行为方式与@synchronized完全不同。

答案 2 :(得分:1)

确定, 我做了一些测试,结果如下:

锁定测试:意思是:2.48661,stdDev:0.50599

synchronized test:mean:2.51298,stdDev:0.49814

调度测试:平均值:2.17046,stdDev: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

答案 3 :(得分:-1)

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

这两种方法都会在你的情况下产生类似的结果,但是对于这样一个简单的解决方案,如使收集线程更安全我更喜欢1。

为什么:

  • 如果您有多个核心,那么多个线程可能同时工作。根据调度程序,他们将在监视器上锁定非常短的时间。

  • 它比分配新块更轻,保留&#39;放入队列(这也是线程同步),并在工作队列准备就绪时执行。

  • 在两种执行方式中都会有很大不同。

  • 如果在某些时候您发现了大量使用集合,您可以考虑将更改锁定更改为读/写类型,如果您使用类似NSLock的类而不是sync_queue则更容易重构/更改。