Какие преимущества имеет 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.