【问题标题】:What advantage(s) does dispatch_sync have over @synchronized?dispatch_sync 比@synchronized 有什么优势?
【发布时间】:2013-07-11 17:06: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 接缝就像首选方式” ...你对那个说法有任何参考吗?
  • @MartinR 我从 GCD WWDC 中感受到这是现代的方式。我可能是错的。
  • 大多数创新都更好......并非总是如此!!!
  • 好的,我想我找到了参考。 @synchronised 在引擎盖下使用锁对吗? “队列不会施加与锁相同的惩罚。例如,将任务排队不需要进入内核来获取互斥锁。” developer.apple.com/library/ios/documentation/General/…
  • 他们不是有完全不同的策略吗?你不能在同一个队列的递归调用中 dispatch_sync,你会死锁,

标签: ios objective-c multithreading thread-safety


【解决方案1】:

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

没那么傻。我的性能测试是错误的。固定的。同时深入研究 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];
    });    
}

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

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

【讨论】:

  • 不错的答案 - 但我并不真正理解增加灵活性的论点 - 你能不能也用 @synchronized 方式做 thing = [thing copy]; 吗?
  • 其实我也不明白'dispatch_sync()'s fast path' 位吗?据我了解,调度队列是线程的包装器。什么情况下会出现快速路径,为什么不使用线程呢?你有它的参考吗?
  • @Robert:切换线程很昂贵,dispatch_sync() 不会使用另一个线程,如果它不需要:gist.github.com/woolsweater/5882484 我不完全知道什么是“不必” " 需要,但libdispatch is open source 如果您愿意看的话。在我的小演示中,原来的线程无论如何都要等待操作的结果,所以切换似乎没有必要。
  • @bbum 在您的方法addThing: 中,您使用的是dispatch_async——而不是dispatch_sync。这是偶然的吗?另一个跟进:假设同步队列没有激烈竞争,实际执行的代码非常小(基本上是一个赋值)。您是否建议在 async 上使用 dispatch sync - 考虑到在同步情况下死锁的额外危险可以被管理?
  • @CouchDeveloper 非常有意。阅读周围的文字。
【解决方案2】:

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

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

【讨论】:

  • 您必须编写自己的 dispatchOnSerialThread 方法,该方法检查当前线程是否 == SerialThread 它在同一线程上执行块,如果不是,则在 SerialThread 上执行 dispatch_sync()。
  • 它也不支持退货,你必须跳__block舞
【解决方案3】:

好的, 我又做了一些测试,结果如下:

锁定测试:平均值: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

【讨论】:

    【解决方案4】:

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

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

    为什么:

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

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

    • 在这两种方法中的执行顺序会有很大不同。

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

    【讨论】:

    • 如果您添加到同步的队列,那么您还需要锁定它。所以你不能从锁中逃跑。获取单个锁比获取锁和管理块(队列)更轻。将任务放入队列涉及块创建/处理。在问题队列中的场景比锁定重得多。
    • OK -- 是的,这个答案不正确。对于同步调度,不需要锁定、分配和保留。而且,一般来说,调度队列比锁轻得多,这正是因为内部实现通常根本不使用锁;更喜欢更有效的原子比较和交换。在没有争用的情况下,在这种简单情况下使用 dispatch_sync() 会快 30%。
    • 上面的例子很好,但这是单线程代码。当我有空闲时间(可能是星期一)时,我会检查多线程方法。上面示例中的简单循环看起来不错,但是我很好奇当多线程发生时它是如何工作的。我仍然不同意你的观点,但我会在更多测试后发表我的最终陈述。
    • 我贴的例子是单队列(单线程)和多队列(多线程)。 Dispatch_sync 在所有情况下都更快。
    猜你喜欢
    • 2014-10-04
    • 2017-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-11
    • 2019-11-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多