Обеспечивает ли @synchronized гарантии безопасности потоков или нет?
Что касается этого answer, мне интересно, это правильно?
@synchronized не делает код "потокобезопасным"
Поскольку я пытался найти любую документацию или ссылку для поддержки этого утверждения, не получится.
Любые комментарии и/или ответы будут оценены по этому поводу.
Для лучшей безопасности потоков мы можем пойти на другие инструменты, это мне известно.
Ответы
Ответ 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
.