Подтвердить что ты не робот

Какое преимущество (-ы) имеет 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];
    }
}
4b9b3361

Ответ 1

Ого. ОК. Моя первоначальная оценка эффективности была ошибочной. Цвет меня глупо.

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

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

Update2: добавлена ​​10-ти очередьная версия каждого типа теста.

OK. Перезапись ответа:

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

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

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

Конечный результат - dispatch_sync() быстрее, чем @synchronized, но не по значимой сумме (на '12 iMac, или '11 mac mini - #s между ними очень разные, btw.. радости concurrency). Использование 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 - reset до 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);

Вышеприведенные просто повторяются для каждого варианта рабочего блока (без использования tricksy runtime-y magic, copypasta FTW!).


Имея это в виду:

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

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

• Используйте dispatch_async() с неизменяемыми данными для сильно конкурирующих ресурсов. То есть:.

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

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

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

Ответ 2

Ok, Я сделал еще несколько тестов, и вот результаты:

блокировка: среднее: 2.48661, stdDev: 0.50599

синхронизированный тест: среднее: 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

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

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

Ответ 4

Есть несколько вещей: 1) @Синхронизация - это тяжелая версия блокировки на каком-то мониторе (я лично предпочитаю NSLock/NSRecursiveLock) 2) Dispatch_sync создает очередь выполнения.

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

Почему:

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

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

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

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