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()
Спасибо.
Ответы
Ответ 1
objc_sync_enter
является примитивом крайне низкого уровня и не предназначен для непосредственного использования. Это деталь реализации старой системы @synchronized
в ObjC. Даже это крайне устарело и, как правило, следует избегать.
Синхронизированный доступ в Cocoa лучше всего достигается с помощью очередей 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" означает, что это единственное, что разрешено запускать в очереди в то время, поэтому все предыдущие чтения будут завершены, и все будущие чтения будут предотвращены до тех пор, пока они не будут завершены. Эта схема означает, что у вас может быть столько одновременных читателей, сколько вы хотите, без голодающих писателей (поскольку писатели всегда будут обслуживаться), а записи никогда не блокируются. При чтении блокируется, и только если есть фактическое утверждение. В обычном, неоспоримом случае это очень быстро.
Ответ 2
Вы уверены, что ваши блоки фактически выполняются на разных потоках?
objc_sync_enter()
/objc_sync_exit()
защищают вас только от доступа к объекту из разных потоков. Они используют рекурсивный мьютекс под капотом, поэтому они не будут либо заторможены, либо не позволят вам повторно обращаться к объекту из того же потока.
Итак, если вы заблокируете один асинхронный блок и разблокируете его в другом, третий блок, выполненный между ними, может иметь доступ к охраняемому объекту.
Ответ 3
Это один из тех тончайших нюансов, который легко пропустить.
Замки в Свифте
Вы должны быть очень осторожны, используя замок. В Swift 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")
}
Это должно заботиться об этом!
Ответ 4
У меня была такая же проблема с использованием очередей в фоновом режиме. Синхронизация не работает все время в очередях с "фоновым" (низким) приоритетом.
Одно исправление, которое я нашел, состояло в том, чтобы использовать семафоры вместо "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.
Ответ 5
В дополнение к решению @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)
}