objc_sync_enter / objc_sync_exit не работает с DISPATCH_QUEUE_PRIORITY_LOW
Мне нужна блокировка чтения \ записи для моего приложения. Я прочитал https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
и написал свой собственный класс, потому что в Swift нет блокировки чтения / записи
class ReadWriteLock {
var logging = true
var b = 0
let r = "vdsbsdbs" // string1 for locking
let g = "VSDBVSDBSDBNSDN" // string2 for locking
func waitAndStartWriting() {
log("wait Writing")
objc_sync_enter(g)
log("enter writing")
}
func finishWriting() {
objc_sync_exit(g)
log("exit writing")
}
// ждет пока все чтение завершится чтобы начать чтение
// и захватить мютекс
func waitAndStartReading() {
log("wait reading")
objc_sync_enter(r)
log("enter reading")
b++
if b == 1 {
objc_sync_enter(g)
log("read lock writing")
}
print("b = \(b)")
objc_sync_exit(r)
}
func finishReading() {
objc_sync_enter(r)
b--
if b == 0 {
objc_sync_exit(g)
log("read unlock writing")
}
print("b = \(b)")
objc_sync_exit(r)
}
private func log(s: String) {
if logging {
print(s)
}
}
}
Это работает хорошо, пока я не попытаюсь использовать его из потоков GCD.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
Когда я пытаюсь использовать этот класс из разных асинхронных блоков в какой-то момент, он позволяет писать, когда запись заблокирована
вот пример журнала:
wait reading
enter reading
read lock writing
b = 1
wait reading
enter reading
b = 2
wait reading
enter reading
b = 3
wait reading
enter reading
b = 4
wait reading
enter reading
b = 5
wait reading
enter reading
b = 6
wait reading
enter reading
b = 7
wait reading
enter reading
b = 8
wait reading
enter reading
b = 9
b = 8
b = 7
b = 6
b = 5
wait Writing
enter writing
exit writing
wait Writing
enter writing
Итак, как вы можете видеть, g был заблокирован, но objc_sync_enter(g) позволяет продолжить. Почему это могло случиться?
Кстати, я проверил, сколько раз был создан ReadWriteLock, и это 1.
Почему objc_sync_exit не работает и позволяет objc_sync_enter(g), когда он не освобожден?
PS Readwirtelock определяется как
class UserData {
static let lock = ReadWriteLock()
Благодарю.
4 ответа
objc_sync_enter
является крайне низкоуровневым примитивом и не предназначен для непосредственного использования. Это деталь реализации старого @synchronized
Система в ObjC. Даже это чрезвычайно устарело и, как правило, его следует избегать.
Синхронизированный доступ в Какао лучше всего достигается с очередями GCD. Например, это общий подход, который обеспечивает блокировку чтения / записи (одновременное чтение, эксклюзивное письмо).
public class UserData {
private let myPropertyQueue = dispatch_queue_create("com.example.mygreatapp.property", DISPATCH_QUEUE_CONCURRENT)
private var _myProperty = "" // Backing storage
public var myProperty: String {
get {
var result = ""
dispatch_sync(myPropertyQueue) {
result = self._myProperty
}
return result
}
set {
dispatch_barrier_async(myPropertyQueue) {
self._myProperty = newValue
}
}
}
}
Все ваши одновременные свойства могут совместно использовать одну очередь, или вы можете назначить каждому свойству свою очередь. Это зависит от того, сколько вы ожидаете конфликтов (писатель заблокирует всю очередь).
"Барьер" в "dispatch_barrier_async" означает, что это единственное, что разрешено запускать в очереди в это время, поэтому все предыдущие чтения будут завершены, и все последующие чтения будут предотвращены до его завершения. Эта схема означает, что у вас может быть столько одновременных читателей, сколько вы хотите, без голодных писателей (поскольку писатели всегда будут обслуживаться), и записи никогда не блокируются. При чтениях блокируются, и только если есть фактические раздоры. В нормальном, неоспоримом случае это очень быстро.
Вы на 100% уверены, что ваши блоки выполняются в разных потоках?
objc_sync_enter()
/ objc_sync_exit()
защищают вас только от доступа к объектам из разных потоков. Они используют рекурсивный мьютекс под колпаком, поэтому они не будут блокировать или препятствовать повторному доступу к объекту из одного и того же потока.
Таким образом, если вы заблокируете один асинхронный блок и разблокируете другой, третий выполненный промежуточный блок может иметь доступ к охраняемому объекту.
Это один из тех тончайших нюансов, который легко пропустить.
Замки в Свифте
Вы должны быть очень осторожны, используя замок. В Свифте String
это структура, то есть передача по значению.
Всякий раз, когда вы звоните objc_sync_enter(g)
Вы не даете это g
, но копия g
, Таким образом, каждый поток по сути создает свою собственную блокировку, которая, по сути, похожа на отсутствие блокировки вообще.
Используйте NSObject
Вместо использования String
или же Int
, используйте равнину NSObject
,
let lock = NSObject()
func waitAndStartWriting() {
log("wait Writing")
objc_sync_enter(lock)
log("enter writing")
}
func finishWriting() {
objc_sync_exit(lock)
log("exit writing")
}
Это должно заботиться об этом!
В дополнение к Rob Napier. Я обновил его до Swift 5.1, добавил универсальную типизацию и несколько удобных методов добавления. Обратите внимание, что только методы, которые обращаются к resultArray через get / set или append, являются потокобезопасными, поэтому я добавил параллельное добавление также для моего случая практического использования, когда данные результатов обновляются во многих вызовах результатов из экземпляров Operation.
public class ConcurrentResultData<E> {
private let resultPropertyQueue = dispatch_queue_concurrent_t.init(label: UUID().uuidString)
private var _resultArray = [E]() // Backing storage
public var resultArray: [E] {
get {
var result = [E]()
resultPropertyQueue.sync {
result = self._resultArray
}
return result
}
set {
resultPropertyQueue.async(group: nil, qos: .default, flags: .barrier) {
self._resultArray = newValue
}
}
}
public func append(element : E) {
resultPropertyQueue.async(group: nil, qos: .default, flags: .barrier) {
self._resultArray.append(element)
}
}
public func appendAll(array : [E]) {
resultPropertyQueue.async(group: nil, qos: .default, flags: .barrier) {
self._resultArray.append(contentsOf: array)
}
}
}
Для примера бега на детской площадке добавьте
//MARK:- helpers
var count:Int = 0
let numberOfOperations = 50
func operationCompleted(d:ConcurrentResultData<Dictionary<AnyHashable, AnyObject>>) {
if count + 1 < numberOfOperations {
count += 1
}
else {
print("All operations complete \(d.resultArray.count)")
print(d.resultArray)
}
}
func runOperationAndAddResult(queue:OperationQueue, result:ConcurrentResultData<Dictionary<AnyHashable, AnyObject>> ) {
queue.addOperation {
let id = UUID().uuidString
print("\(id) running")
let delay:Int = Int(arc4random_uniform(2) + 1)
for _ in 0..<delay {
sleep(1)
}
let dict:[Dictionary<AnyHashable, AnyObject>] = [[ "uuid" : NSString(string: id), "delay" : NSString(string:"\(delay)") ]]
result.appendAll(array:dict)
DispatchQueue.main.async {
print("\(id) complete")
operationCompleted(d:result)
}
}
}
let q = OperationQueue()
let d = ConcurrentResultData<Dictionary<AnyHashable, AnyObject>>()
for _ in 0..<10 {
runOperationAndAddResult(queue: q, result: d)
}
У меня была такая же проблема с использованием очередей в фоновом режиме. Синхронизация не работает все время в очередях с "фоновым" (низким) приоритетом.
Одно исправление, которое я нашел, состояло в том, чтобы использовать семафоры вместо "obj_sync":
static private var syncSemaphores: [String: DispatchSemaphore] = [:]
static func synced(_ lock: String, closure: () -> ()) {
//get the semaphore or create it
var semaphore = syncSemaphores[lock]
if semaphore == nil {
semaphore = DispatchSemaphore(value: 1)
syncSemaphores[lock] = semaphore
}
//lock semaphore
semaphore!.wait()
//execute closure
closure()
//unlock semaphore
semaphore!.signal()
}
Идея функции исходит из того, что Swift эквивалентно "@synchronized" Objective-C?, ответ @bryan-mclemore.