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

Обеспечивает ли @synchronized гарантии безопасности потоков или нет?

Что касается этого answer, мне интересно, это правильно?

@synchronized не делает код "потокобезопасным"

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

Любые комментарии и/или ответы будут оценены по этому поводу.

Для лучшей безопасности потоков мы можем пойти на другие инструменты, это мне известно.

4b9b3361

Ответ 1

@synchronized делает код безопасным, если он используется правильно.

Например:

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

Так что скажем, у меня есть два метода. storeData: и readData в одноэлементном классе под названием LocalStore.

- (void)storeData:(NSData *)data
 {
      [self writeDataToDisk:data];
 }

 - (NSData *)readData
 {
     return [self readDataFromDisk];
 }

Теперь, если бы я отправлял каждый из этих методов в свой собственный поток, например:

 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      [[LocalStore sharedStore] storeData:data];
 });
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      [[LocalStore sharedStore] readData];
 });

Скорее всего, мы потерпим крах. Однако, если мы изменим наши методы storeData и readData, чтобы использовать @synchronized

 - (void)storeData:(NSData *)data
 {
     @synchronized(self) {
       [self writeDataToDisk:data];
     }
 }

 - (NSData *)readData
 { 
     @synchronized(self) {
      return [self readDataFromDisk];
     }
 }

Теперь этот код будет потокобезопасным. Важно отметить, что если я удалю один из операторов @synchronized, однако код больше не будет потокобезопасным. Или, если мне нужно синхронизировать разные объекты вместо self.

@synchronized создает блокировку мьютекса на объекте, который вы синхронизируете. Таким образом, другими словами, если какой-либо код хочет получить доступ к коду в блоке @synchronized(self) { }, ему придется встать в очередь за всем предыдущим кодом, запущенным внутри этого же блока.

Если бы мы создавали разные объекты localStore, @synchronized(self) блокировал бы каждый объект отдельно. Это имеет смысл?

Подумайте об этом так. У вас целая группа людей, ожидающих в отдельных строках, каждая строка пронумерована 1-10. Вы можете выбрать, какую строку вы хотите, чтобы каждый человек мог подождать (синхронизировавшись по строке), или если вы не используете @synchronized, вы можете перейти прямо к фронту и пропустить все строки. Лицу в строке 1 не нужно ждать, пока человек в строке 2 закончит, но человек в строке 1 должен ждать, пока все перед ними в своей строке не закончатся.

Ответ 2

Я думаю, что суть вопроса:

- это правильное использование синхронизации, способной решать любые поточно-безопасные проблема?

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


Я отвечу, не допуская прежних знаний.

Правильный код - это код, соответствующий его спецификации. Хорошая спецификация определяет

  • инварианты, сдерживающие состояние,
  • предварительные условия и постусловия, описывающие последствия операций.

Защищенный от кода код - это код, который остается верным при выполнении несколькими потоками. Таким образом,

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

Точка выгрузки на высоком уровне: потокобезопасная требует, чтобы спецификация сохранялась в течение многопоточного исполнения. Чтобы на самом деле закодировать это, мы должны сделать только одно: регулировать доступ к изменяемому общему состоянию 3. И есть три способа сделать это:

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

Первые две простые. Третий требует предотвращения следующих проблем безопасности потока:

  • живучести
    • тупик: блок двух потоков постоянно ждет друг друга, чтобы освободить необходимый ресурс.
    • livelock: поток занят работой, но не может добиться какого-либо прогресса.
    • голодание: поток постоянно отказывает в доступе к ресурсам, которые ему нужны, чтобы добиться прогресса.
  • безопасная публикация: как ссылка, так и состояние опубликованного объекта должны быть видны другим потокам одновременно.
  • Условия гонки Состояние гонки - это дефект, при котором выход зависит от времени неконтролируемых событий. Другими словами, условие гонки происходит, когда правильный ответ зависит от счастливого времени. Любая составная операция может выдержать состояние гонки, например: "check-then-act", "put-if-absent". Примерной проблемой будет if (counter) counter--;, и одним из нескольких решений будет @synchronize(self){ if (counter) counter--;}.

Для решения этих проблем мы используем такие инструменты, как @synchronize, volatile, барьеры памяти, атомные операции, специальные блокировки, очереди и синхронизаторы (семафоры, барьеры).

И вернемся к вопросу:

- это правильное использование @synchronize, способного решать любые поточно-безопасные проблема?

Технически да, потому что любой инструмент, упомянутый выше, можно эмулировать с помощью @synchronize. Но это приведет к низкой производительности и увеличению вероятности проблем, связанных с живучестью. Вместо этого вам нужно использовать соответствующий инструмент для каждой ситуации. Пример:

counter++;                       // wrong, compound operation (fetch,++,set)
@synchronize(self){ counter++; } // correct but slow, thread contention
OSAtomicIncrement32(&count);     // correct and fast, lockless atomic hw op

В случае связанного вопроса вы действительно можете использовать @synchronize или блокировку чтения-записи GCD или создать коллекцию с блокировкой блокировки или что бы ни требовала ситуация. Правильный ответ зависит от шаблона использования. Как вы это делаете, вы должны документировать в своем классе, какие поточные гарантии вы предлагаете.


1 То есть, посмотрите объект в недопустимом состоянии или нарушите условия pre/post.

2 Например, если поток A выполняет итерацию коллекции X, а поток B удаляет элемент, выполнение происходит сбой. Это не потокобезопасно, потому что клиенту придется синхронизировать внутреннюю блокировку X (synchronize(X)), чтобы иметь эксклюзивный доступ. Однако, если итератор возвращает копию коллекции, коллекция становится потокобезопасной.

3 Неизменяемое разделяемое состояние или изменяемые не общие объекты всегда являются потокобезопасными.

Ответ 3

Как правило, @synchronized гарантирует безопасность потока, но только при правильном использовании. Также безопасно приобретать блокировку рекурсивно, хотя и с ограничениями, которые я подробно описываю в своем ответе здесь.

Существует несколько распространенных способов использования @synchronized. Они наиболее распространены:

Использование @synchronized для обеспечения создания атомного объекта.

- (NSObject *)foo {
    @synchronized(_foo) {
        if (!_foo) {
            _foo = [[NSObject alloc] init];
        }
        return _foo;
    }
}

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

Использование @synchronized для блокировки нового объекта каждый раз.

- (void)foo {
    @synchronized([[NSObject alloc] init]) {
        [self bar];
    }
}

Я видел этот код совсем немного, а также эквивалент С# lock(new object()) {..}. Поскольку он каждый раз пытается заблокировать новый объект, он всегда будет разрешен в критическом разделе кода. Это не какая-то кодовая магия. Это не делает ничего для обеспечения безопасности потоков.

Наконец, блокировка на self.

- (void)foo {
    @synchronized(self) {
        [self bar];
    }
}

Хотя сама по себе проблема, если ваш код использует какой-либо внешний код или сам является библиотекой, это может быть проблемой. Хотя внутри объект известен как self, он внешне имеет имя переменной. Если внешний код вызывает @synchronized(_yourObject) {...}, и вы вызываете @synchronized(self) {...}, вы можете оказаться в тупике. Лучше всего создать внутренний объект для блокировки, который не отображается вне вашего объекта. Добавление _lockObject = [[NSObject alloc] init]; внутри вашей функции init дешево, легко и безопасно.

EDIT:

Мне все еще задают вопросы об этом сообщении, так что вот пример того, почему на практике использовать @synchronized(self) плохую идею.

@interface Foo : NSObject
- (void)doSomething;
@end

@implementation Foo
- (void)doSomething {
    sleep(1);
    @synchronized(self) {
        NSLog(@"Critical Section.");
    }
}

// Elsewhere in your code
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
Foo *foo = [[Foo alloc] init];
NSObject *lock = [[NSObject alloc] init];

dispatch_async(queue, ^{
    for (int i=0; i<100; i++) {
        @synchronized(lock) {
            [foo doSomething];
        }
        NSLog(@"Background pass %d complete.", i);
    }
});

for (int i=0; i<100; i++) {
    @synchronized(foo) {
        @synchronized(lock) {
            [foo doSomething];
        }
    }
    NSLog(@"Foreground pass %d complete.", i);
}

Должно быть очевидно, почему это происходит. Блокировка на foo и lock вызывается в разных порядках на передних потоках VS-фона. Легко сказать, что это плохая практика, но если foo является библиотекой, пользователь вряд ли узнает, что код содержит блокировку.

Ответ 4

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

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

Ответ 5

@synchronized - механизм thread safe. Часть кода, написанная внутри этой функции, становится частью critical section, к которой может выполняться только один поток за раз.

@synchronize применяет блокировку неявно, тогда как NSLock применяет ее явно.

Это только гарантирует безопасность потока, а не гарантирует это. Я имею в виду, что вы нанимаете опытного водителя для своей машины, но это не гарантирует, что автомобиль не встретит несчастного случая. Однако вероятность остается малейшей.

Это компаньон в GCD (большая центральная отправка) dispatch_once. dispatch_once выполняет ту же работу, что и для @synchronized.

Ответ 6

Директива @synchronized - это удобный способ создания блокировок мьютексов "на лету" в коде Objective-C.

побочные эффекты блокировок мьютексов:

  • взаимоблокировки
  • голод

Безопасность потока будет зависеть от использования блока @synchronized.