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.

Другие вопросы по тегам