Блокировка объекта от доступа несколькими потоками - Objective-C
У меня вопрос о безопасности потоков в Objective-C. Я прочитал пару других ответов, некоторые из документации Apple, и все еще есть некоторые сомнения в этом, поэтому подумал, что я задам свой вопрос.
Мой вопрос три раза:
Предположим, что у меня есть массив, NSMutableArray *myAwesomeArray;
Сложить 1:
Теперь исправьте меня, если я ошибаюсь, но из того, что я понимаю, использование @synchronized(myAwesomeArray){...}
предотвратит доступ двух потоков к одному и тому же блоку кода. Итак, в основном, если у меня есть что-то вроде:
-(void)doSomething {
@synchronized(myAwesomeArray) {
//some read/write operation on myAwesomeArray
}
}
тогда, если два потока обращаются к одному и тому же методу с тем же временем, этот блок кода будет потокобезопасным. Я предполагаю, что я правильно понял эту часть.
Сложить 2:
Что мне делать, если к myAwesomeArray
обращаются несколько потоков из разных методов?
Если у меня есть что-то вроде:
- (void)readFromArrayAccessedByThreadOne {
//thread 1 reads from myAwesomeArray
}
- (void)writeToArrayAccessedByThreadTwo {
//thread 2 writes to myAwesomeArray
}
Теперь оба метода получают доступ к двум различным потокам одновременно. Как я могу гарантировать, что myAwesomeArray
не будет иметь проблем? Использую ли я что-то вроде NSLock или NSRecursiveLock?
Сложить 3:
Теперь, в вышеупомянутых двух случаях, myAwesomeArray
был iVar в памяти. Что делать, если у меня есть файл базы данных, который я не всегда храню в памяти. Я создаю databaseManagerInstance
всякий раз, когда я хочу выполнять операции с базой данных, и отпустите его, как только закончите. Таким образом, в основном разные классы могут обращаться к базе данных. Каждый класс создает свой собственный экземпляр DatabaseManger
, но в основном все они используют один и тот же файл с одной базой данных. Как я могу гарантировать, что данные не будут повреждены из-за условий гонки в такой ситуации?
Это поможет мне понять некоторые из моих основ.
Ответы
Ответ 1
Сложить 1
Обычно ваше понимание того, что делает @synchronized
, является правильным. Однако технически он не делает код "потокобезопасным". Это предотвращает одновременное использование разных потоков одним и тем же замком, однако вам необходимо убедиться, что вы всегда используете один и тот же токен синхронизации при выполнении критических разделов. Если вы этого не сделаете, вы все равно можете оказаться в ситуации, когда два потока одновременно выполняют критические разделы. Проверьте документы.
Сложить 2
Большинство людей, вероятно, посоветуют вам использовать NSRecursiveLock. Если бы я был вами, я бы использовал GCD. Вот отличный документ, показывающий, как перейти от программирования потоков к программированию GCD. Я думаю, что этот подход к проблеме намного лучше, чем тот, который основан на на NSLock. В двух словах вы создаете очередную очередь и отправляете свои задачи в эту очередь. Таким образом, вы гарантируете, что ваши критические секции обрабатываются последовательно, поэтому в любой момент времени выполняется только один критический раздел.
Сложить 3
Это то же самое, что и Fold 2, только более конкретный. База данных - это ресурс, во многом это то же самое, что и массив или любая другая вещь. Если вы хотите увидеть подход на основе GCD в контексте программирования баз данных, взгляните на реализацию fmdb. Он делает то, что я описал в Fold2.
Как побочная заметка для Fold 3, я не думаю, что создание базы данных DatabaseManager каждый раз, когда вы хотите использовать базу данных, а затем освобождение - это правильный подход. Я думаю, вам нужно создать одно соединение с базой данных и сохранить его через сеанс приложения. Таким образом, проще управлять им. Опять же, fmdb - отличный пример того, как это можно достичь.
Edit
Если вы не хотите использовать GCD, тогда да, вам нужно будет использовать какой-то механизм блокировки, и да, NSRecursiveLock
предотвратит взаимоблокировки, если вы используете рекурсию в своих методах, поэтому это хороший выбор (используется @synchronized
). Однако может быть один улов. Если возможно, что многие потоки будут ждать одного и того же ресурса, и порядок, в котором они получают доступ, имеет значение, тогда NSRecursiveLock
недостаточно. Вы все еще можете справиться с этой ситуацией с помощью NSCondition
, но поверьте мне, вы сэкономите много времени, используя GCD в этом случае. Если порядок потоков не имеет значения, вы можете быть уверены в блокировке.
Ответ 2
Как и в Swift 3 в WWDC 2016 Session Session 720 Параллельное программирование С GCD в Swift 3, вы должны использовать queue
class MyObject {
private let internalState: Int
private let internalQueue: DispatchQueue
var state: Int {
get {
return internalQueue.sync { internalState }
}
set (newValue) {
internalQueue.sync { internalState = newValue }
}
}
}
Ответ 3
Подкласс NSMutableArray для обеспечения блокировки для методов доступа (чтения и записи). Что-то вроде:
@interface MySafeMutableArray : NSMutableArray { NSRecursiveLock *lock; } @end
@implementation MySafeMutableArray
- (void)addObject:(id)obj {
[self.lock lock];
[super addObject: obj];
[self.lock unlock];
}
// ...
@end
Этот подход инкапсулирует блокировку как часть массива. Пользователям не нужно менять свои вызовы (но, возможно, им необходимо знать, что они могут блокировать/ждать доступа, если доступ критически важен). Существенным преимуществом такого подхода является то, что если вы решите, что не хотите использовать блокировки, вы можете повторно реализовать MySafeMutableArray для использования очередей отправки - или что-то лучшее для вашей конкретной проблемы. Например, вы можете реализовать addObject как:
- (void)addObject:(id)obj {
dispatch_sync (self.queue, ^{ [super addObject: obj] });
}
Примечание. Если вы используете блокировки, вам наверняка понадобится NSRecursiveLock, а не NSLock, потому что вы не знаете реализации Objective-C addObject и т.д. сами рекурсивные.