Ответ 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];
});
}
Наконец, не важно, какой из них вы используете для поддержания содержимого массива. Стоимость синдицированных сделок чрезвычайно велика. Для асинхронного случая стоимость конкуренции идет вниз, но потенциал для сложности или странных проблем с производительностью идет вверх.
При проектировании параллельных систем лучше всего поддерживать границу между очередями как можно меньше. Большая часть этого заключается в том, чтобы как можно меньше ресурсов "жить" с обеих сторон границы.