Какие преимущества имеет dispatch_sync по сравнению с @synchronized?

Допустим, я хочу сделать этот код потокобезопасным:

- (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 ответа

Решение

Вот это да. ОК. Моя первоначальная оценка производительности была неверной. Раскрась меня глупо.

Не так глупо. Мой тест производительности был неверным. Исправлена. Вместе с глубоким погружением в код GCD.

Обновление: код для эталонного теста можно найти здесь: https://github.com/bbum/Stackru Надеюсь, теперь он правильный.:)

Обновление 2: добавлена ​​версия с 10 очередями для каждого вида теста.

ХОРОШО. Переписать ответ:

@synchronized() был вокруг в течение длительного времени. Это реализовано как поиск хеша, чтобы найти блокировку, которая затем блокируется. Это "довольно быстро" - как правило, достаточно быстро - но может быть бременем при высокой конкуренции (как и любой примитив синхронизации).

dispatch_sync() не обязательно требует блокировки и не требует, чтобы блок был скопирован. В частности, в случае быстрого пути, dispatch_sync() вызовет блок непосредственно в вызывающем потоке без копирования блока. Даже в случае медленного пути блок не будет скопирован, так как вызывающий поток должен блокировать до выполнения в любом случае (вызывающий поток приостанавливается до тех пор, пока какая-либо работа не опережает dispatch_sync() закончено, затем поток возобновляется). Единственным исключением является вызов в главной очереди / потоке; в этом случае блок по-прежнему не копируется (потому что вызывающий поток приостановлен и, следовательно, использование блока из стека - это нормально), но для постановки в очередь в главную очередь, выполнения и затем возобновите вызывающий поток.

dispatch_async() требуется, чтобы блок был скопирован, так как он не может выполняться в текущем потоке, и при этом текущий поток не может быть заблокирован (поскольку блок может немедленно заблокировать некоторый локальный ресурс потока, который становится доступным только в строке кода после dispatch_async(), Хотя дорого, dispatch_async() перемещает работу из текущего потока, позволяя немедленно возобновить выполнение.

Конечный результат -- dispatch_sync() быстрее чем @synchronized, но не в общем значимом количестве (на iMac '12, ни на mac11 '11 между ними очень разные, кстати... радости параллелизма). С помощью 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);

Вышеупомянутое просто повторяется для каждого варианта рабочего модуля (не используется хитрая магия времени выполнения-y; copypasta FTW!).


С этим в мыслях:

• использовать @synchronized() если вам нравится, как это выглядит. Реальность такова, что если ваш код конкурирует с этим массивом, у вас, вероятно, есть проблема с архитектурой. Примечание: использование @synchronized(someObject) может иметь непреднамеренные последствия в том, что это может вызвать дополнительную конкуренцию, если объект используется внутри @synchronized(self)!

• использовать dispatch_sync() с последовательной очередью, если это ваша вещь. Там нет накладных расходов - это на самом деле быстрее, как утверждал и uncontended случае - и с использованием очереди и легче для отладки и проще в профиль в том, что инструменты и Debugger оба имеют отличные инструменты для отладки очередей (и они становятся лучше все время) в то время как отладка блокировок может быть болью.

• использовать dispatch_async() с неизменными данными для сильно загруженных ресурсов. То есть:

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

Наконец, не должно иметь значения, какой из них вы используете для поддержки содержимого массива. Стоимость спора чрезвычайно высока для синхронных случаев. В асинхронном случае цена конфликта значительно снижается, но вероятность сложности или странных проблем с производительностью возрастает.

При проектировании параллельных систем лучше всего поддерживать границу между очередями как можно меньше. Большая часть этого - обеспечение того, чтобы как можно меньше ресурсов "жили" по обе стороны границы.

Я обнаружил, что dispatch_sync() - плохой способ блокировки, он не поддерживает вложенные вызовы.

Поэтому вы не можете вызвать dispatch_sync для последовательного Q, а затем вызвать его снова в подпрограмме с тем же Q. Это означает, что он не ведет себя так же, как @synchronized вообще.

Хорошо, я сделал еще несколько тестов и вот результаты:

тест блокировки: среднее значение: 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) @Synchronize - это тяжелая версия блокировки на каком-то мониторе (я лично предпочитаю NSLock/NSRecursiveLock) 2) Dispatch_sync создает очередь выполнения.

оба подхода приводят к аналогичному результату в вашем случае, однако для такого простого решения, как обеспечение безопасности потока коллекции, я бы предпочел 1.

Зачем:

  • Если у вас несколько ядер, то несколько потоков могут работать одновременно. В зависимости от планировщика они будут очень быстро блокироваться на мониторе.

  • это намного легче, чем выделение нового блока, сохранение "вещи", помещаемой в очередь (это также синхронизируется с потоками), и выполнение, когда рабочая очередь готова.

  • в обоих подходах порядок исполнения будет очень разным.

  • Если в какой-то момент вы обнаружите интенсивное использование коллекции, вы можете рассмотреть возможность изменения блокировки на тип чтения / записи, что намного проще для рефакторинга / изменения, если вы используете какой-то NSLock-подобный класс вместо sync_queue.

Другие вопросы по тегам