31

假设我想让这段代码线程安全:

- (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

4 回答 4

51

哇。好的——我最初的绩效评估完全错误。给我涂上愚蠢的颜色。

没那么傻。我的性能测试是错误的。固定的。以及深入研究 GCD 代码。

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

更新 2:添加了每种测试的 10 队列版本。

好的。重写答案:

• <code>@synchronized() 已经存在很长时间了。它被实现为哈希查找以查找然后锁定的锁。它“相当快”——通常足够快——但在高竞争下可能是一种负担(任何同步原语都可能如此)。

dispatch_sync()不一定需要锁,也不需要复制块。具体来说,在快速路径的情况下,dispatch_sync()将直接在调用线程上调用块,而不复制块。即使在慢速路径的情况下,也不会复制块,因为调用线程必须阻塞直到执行(调用线程被挂起,直到dispatch_sync()完成之前的任何工作,然后线程恢复)。一个例外是在主队列/线程上调用;在这种情况下,该块仍然没有被复制(因为调用线程被挂起,因此,使用堆栈中的一个块是可以的),但是有很多工作完成了在主队列中排队、执行和然后恢复调用线程。

• <code>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()如果您喜欢它的外观,请使用。现实情况是,如果您的代码在该数组上竞争,您可能遇到了架构问题。 注意: using@synchronized(someObject)可能会产生意想不到的后果,因为如果对象内部使用它可能会导致额外的争用@synchronized(self)

dispatch_sync()如果您愿意,请与串行队列一起使用。没有开销——在竞争和非竞争的情况下它实际上都更快——并且使用队列更容易调试和更容易分析,因为 Instruments 和 Debugger 都有用于调试队列的优秀工具(而且它们正在变得更好一直),而调试锁可能会很痛苦。

dispatch_async()与不可变数据一起用于竞争激烈的资源。IE:

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

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

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

于 2013-07-11T17:54:29.050 回答
2

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

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

于 2015-12-12T19:10:30.170 回答
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
于 2013-07-16T07:38:22.033 回答
-1

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

在您的情况下,这两种方法都会导致类似的结果,但是对于像使收集线程安全这样的简单解决方案,我更喜欢 1.

为什么:

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

  • 它比分配新块要轻得多,保留“事物”放入队列(这也是线程同步的),并在工作队列准备好时执行。

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

  • 如果在某个时候您发现大量使用集合,您可能会考虑将锁更改为读/写类型,如果您使用一些类似 NSLock 的类而不是 sync_queue,则重构/更改要简单得多。

于 2013-07-11T19:59:02.720 回答