Що Swift еквівалентно "@синхронізованому" Objective-C?


231

Я шукав книгу Swift, але не можу знайти версію Swift @synchronized. Як зробити взаємне виключення у Swift?


1
Я б використав диспетчерський бар'єр. Бар'єри забезпечують дуже дешеву синхронізацію. dispatch_barrier_async (). тощо
Фредерік К. Лі

@ FrederickC.Lee, що робити, якщо вам потрібно записати для синхронізації, як, наприклад, під час створення обгортки removeFirst()?
ScottyBlades

Відповіді:


183

Можна використовувати GCD. Це трохи більше, ніж багатослівний @synchronized, але працює як заміна:

let serialQueue = DispatchQueue(label: "com.test.mySerialQueue")
serialQueue.sync {
    // code
}

12
Це чудово, але не вистачає можливостей повторного входу, які ви маєте за допомогою @synchronized.
Водоспад Майкл

9
З таким підходом потрібно бути обережним. Ваш блок може бути виконаний на якомусь іншому потоці. Документи API кажуть: "Для оптимізації ця функція викликає блок на поточному потоці, коли це можливо".
біо

20
Чудова стаття від Метта Галлахера про це: cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html
wuf810

4
Ні, це призводить до випадкових тупиків.
Том Крайна

70
Ні, ні і ні. Приємна спроба, але працює недосконало. Чому? Основне читання (всебічне порівняння альтернатив, застережень) та чудова корисна рамка від Метта Галлахера тут: cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html @ wuf810 згадав про це першим (HT), але занижена наскільки хороша ця стаття. Усі повинні прочитати. (Будь ласка, підкресліть це мінімумом, щоб воно було спочатку видимим, але не більше.)
t0rst

181

Я сам це шукав і прийшов до висновку, що для цього ще немає рідної конструкції.

Я зробив цю маленьку помічну функцію на основі коду, який я бачив від Метта Бріджеса та інших.

func synced(_ lock: Any, closure: () -> ()) {
    objc_sync_enter(lock)
    closure()
    objc_sync_exit(lock)
}

Використання досить прямо вперед

synced(self) {
    println("This is a synchronized closure")
}

З цією проблемою я знайшов одну проблему. Передача масиву як аргумент блокування, здається, викликає дуже тупу помилку компілятора. Інакше, хоча, здається, працює так, як хочеться.

Bitcast requires both operands to be pointer or neither
  %26 = bitcast i64 %25 to %objc_object*, !dbg !378
LLVM ERROR: Broken function found, compilation aborted!

Приємно! Будь ласка, подайте помилку для цього, якщо це все-таки проблема в 1.0
MattD

14
Це досить корисно і добре зберігає синтаксис @synchronizedблоку, але зауважте, що він не є ідентичним справжньому вбудованому блоку оператора, як @synchronizedблоку в Objective-C, оскільки returnі breakзаяви більше не працюють, щоб вискочити з навколишньої функції / циклу, як якби це була звичайна заява.
newacct

3
Помилка, ймовірно, пов’язана з тим, що масиви передаються як значення, а не посилання
james_alvarez

9
Можливо, це буде чудовим місцем для використання нового deferключового слова, щоб забезпечити objc_sync_exitвиклик, навіть якщо closureкидки.
devios1

3
@ t0rst Якщо називати цю відповідь "помилковою" на основі пов'язаної статті, не вірно. У статті йдеться, що цей метод "трохи повільніше, ніж ідеальний" і "обмежений платформами Apple". Це не робить його "хибним" довгим пострілом.
RenniePet

150

Мені подобаються і використовую тут багато відповідей, тому я б обрав те, що найкраще підходить для вас. Це означає, що метод, який я віддаю перевагу, коли мені потрібно щось на зразок aim-c, @synchronizedвикористовує deferоператор, введений у швидкій 2.

{ 
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }

    //
    // code of critical section goes here
    //

} // <-- lock released when this block is exited

Хороша річ про цей метод є те , що ваша критична секція може вийти з блоку , що містить яким - небудь чином бажаного (наприклад, return, break, continue, throw), а також «заяви в рамках заяви Defer виконуються незалежно від того , яким чином передається управління програмою.» 1


Я думаю, що це, мабуть, найвишуканіше рішення, що надається тут. Дякуємо за ваш відгук.
Скотт Д

3
Що таке lock? Як lockініціалізується?
Van Du Tran

6
lockє будь-який об'єкт-c об'єктом.
ɲeuroburɳ

1
Відмінно! Коли було запроваджено Swift 1, я написав кілька допоміжних методів блокування, і не переглядав їх протягом певного часу. Повністю забув про відстрочку; це шлях!
Ренді

Мені це подобається, але я отримую помилку компілятора "Брезований блок операторів - це невикористане закриття" в Xcode 8. А я розумію, що вони є лише функційними дужками - теж час, щоб знайти ваше "1" посилання - дякую!
Данкан Гроневальд

83

Ви можете сендвіч заяви між objc_sync_enter(obj: AnyObject?)і objc_sync_exit(obj: AnyObject?). Ключове слово @synchronized використовує ці методи під обкладинками. тобто

objc_sync_enter(self)
... synchronized code ...
objc_sync_exit(self)

3
Чи буде це вважатись використанням приватного API Apple?
Друкс

2
Ні, objc_sync_enterі objc_sync_exitце методи, визначені в Objc-sync.h і є відкритим кодом: opensource.apple.com/source/objc4/objc4-371.2/runtime/…
bontoJR

Що станеться, якщо кілька потоків намагаються отримати доступ до одного і того ж ресурсу, другий чекає, повторює чи збої?
TruMan1

Додаючи до сказаного @bontoJR, objc_sync_enter(…)& objc_sync_exit(…)публічні заголовки надаються iOS / macOS / тощо. API (схоже, вони знаходяться всередині ….sdkшляху usr/include/objc/objc-sync.h) . Найпростіший спосіб з'ясувати, чи є щось публічним API чи ні - це (в Xcode) ввести ім'я функції (наприклад objc_sync_enter(); аргументи не потрібно вказувати для функцій C) , а потім спробуйте клацнути її командою. Якщо він показує вам файл заголовка для цього API, то ви хороші (оскільки ви не змогли б побачити заголовок, якби він не був загальнодоступним) .
Сліпп Д. Томпсон,

75

Аналог @synchronizedдирективи від Objective-C може мати довільний тип повернення та гарну rethrowsповедінку у Swift.

// Swift 3
func synchronized<T>(_ lock: AnyObject, _ body: () throws -> T) rethrows -> T {
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }
    return try body()
}

Використання deferоператора дозволяє безпосередньо повернути значення без введення тимчасової змінної.


У Swift 2 додайте @noescapeатрибут до закриття, щоб дозволити більше оптимізації:

// Swift 2
func synchronized<T>(lock: AnyObject, @noescape _ body: () throws -> T) rethrows -> T {
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }
    return try body()
}

Виходячи з відповідей GNewc [1] (де мені подобається довільний тип повернення) та Тода Каннінгама [2] (де мені подобається defer).


Xcode повідомляє мені, що стандарт @noescape тепер за замовчуванням і застарілий у Swift 3.
RenniePet

Правильно, код у цій відповіді призначений для Swift 2 і вимагає певної адаптації для Swift 3. Я оновлю його, коли встигну.
werediver

1
Чи можете ви пояснити використання? Можливо, з прикладом .. заздалегідь дякую! У моєму випадку у мене є набір, який мені потрібно синхронізувати, оскільки я маніпулюю його вмістом у DispatchQueue.
Санчо

@sancho Я вважаю за краще, щоб ця публікація була лаконічною. Ви, здається, запитуєте про загальні паралельні правила програмування, це питання широке. Спробуйте задати це як окреме питання!
werediver

41

SWIFT 4

У Swift 4 ви можете використовувати черги відправлення GCD для блокування ресурсів.

class MyObject {
    private var internalState: Int = 0
    private let internalQueue: DispatchQueue = DispatchQueue(label:"LockingQueue") // Serial by default

    var state: Int {
        get {
            return internalQueue.sync { internalState }
        }

        set (newState) {
            internalQueue.sync { internalState = newState }
        }
    }
} 

Схоже, це не працює з XCode8.1. .serialздається, недоступний. Але .concurrentє в наявності. : /
Тревіс Гріггс

2
за замовчуванням .serial
Duncan

2
Зауважте, що ця модель не захищає належним чином від більшості поширених проблем із багатопотоковими потоками. Наприклад, якщо ви працюєте myObject.state = myObject.state + 1одночасно, він би не рахував загальних операцій, а натомість давав би недетерміноване значення. Щоб вирішити цю проблему, викликовий код слід загорнути в послідовну чергу, щоб і читання, і запис відбувалися атомно. Звичайно, у Obj-c @synchronisedє та сама проблема, тож у цьому сенсі ваша реалізація є правильною.
Берік

1
Так, myObject.state += 1це комбінація операції читання, а потім запису. Деякі інші нитки все ще можуть входити між ними, щоб встановити / записати значення. Відповідно до objc.io/blog/2018/12/18/atomic-variables , було б легше запустити setблок синхронізації / закриття замість цього, а не під самою змінною.
CyberMew

23

Щоб додати функцію повернення, ви можете зробити це:

func synchronize<T>(lockObj: AnyObject!, closure: ()->T) -> T
{
  objc_sync_enter(lockObj)
  var retVal: T = closure()
  objc_sync_exit(lockObj)
  return retVal
}

Згодом ви можете викликати це за допомогою:

func importantMethod(...) -> Bool {
  return synchronize(self) {
    if(feelLikeReturningTrue) { return true }
    // do other things
    if(feelLikeReturningTrueNow) { return true }
    // more things
    return whatIFeelLike ? true : false
  }
}

23

Використовуючи відповідь Брайана МакЛемора, я розширив його для підтримки об’єктів, які кидають у безпечну садибу з можливістю відкладати Swift 2.0.

func synchronized( lock:AnyObject, block:() throws -> Void ) rethrows
{
    objc_sync_enter(lock)
    defer {
        objc_sync_exit(lock)
    }

    try block()
}

Було б краще використовувати rethrowsдля спрощення використання із закритими замиканнями (не потрібно використовувати try), як показано у моїй відповіді .
werediver

10

Швидкий 3

Цей код має можливість повторного введення і може працювати з асинхронними викликами функцій. У цьому коді після виклику someAsyncFunc () інша функція закриття послідовної черги буде оброблятися, але блокується semaphore.wait () до виклику сигналу (). InternalQueue.sync не слід використовувати, оскільки він заблокує основний потік, якщо я не помиляюся.

let internalQueue = DispatchQueue(label: "serialQueue")
let semaphore = DispatchSemaphore(value: 1)

internalQueue.async {

    self.semaphore.wait()

    // Critical section

    someAsyncFunc() {

        // Do some work here

        self.semaphore.signal()
    }
}

objc_sync_enter / objc_sync_exit не є хорошою ідеєю без обробки помилок.


Яка обробка помилок? Компілятор не дозволить нічого, що кидає. З іншого боку, не використовуючи objc_sync_enter / exit, ви відмовляєтесь від суттєвих підвищення продуктивності.
gnasher729

8

У сесії 414 "Розуміння збоїв та журналів збоїв" 414 WWDC 2018 року показано наступний спосіб, використовуючи DispatchQueues з синхронізацією.

У swift 4 має бути щось таке:

class ImageCache {
    private let queue = DispatchQueue(label: "sync queue")
    private var storage: [String: UIImage] = [:]
    public subscript(key: String) -> UIImage? {
        get {
          return queue.sync {
            return storage[key]
          }
        }
        set {
          queue.sync {
            storage[key] = newValue
          }
        }
    }
}

У будь-якому разі ви також можете зробити читання швидше, використовуючи паралельні черги з бар'єрами. Читання синхронізації та асинхронізації виконуються одночасно, а запис нового значення чекає завершення попередніх операцій.

class ImageCache {
    private let queue = DispatchQueue(label: "with barriers", attributes: .concurrent)
    private var storage: [String: UIImage] = [:]

    func get(_ key: String) -> UIImage? {
        return queue.sync { [weak self] in
            guard let self = self else { return nil }
            return self.storage[key]
        }
    }

    func set(_ image: UIImage, for key: String) {
        queue.async(flags: .barrier) { [weak self] in
            guard let self = self else { return }
            self.storage[key] = image
        }
    }
}

вам, ймовірно, не потрібно блокувати читання та уповільнювати чергу, використовуючи синхронізацію. Ви можете просто використовувати синхронізацію для послідовного запису.
Basheer_CAD

6

Використовуйте NSLock у Swift4:

let lock = NSLock()
lock.lock()
if isRunning == true {
        print("Service IS running ==> please wait")
        return
} else {
    print("Service not running")
}
isRunning = true
lock.unlock()

Попередження Клас NSLock використовує потоки POSIX для реалізації своєї поведінки блокування. Відправляючи повідомлення про розблокування на об’єкт NSLock, ви повинні бути впевнені, що повідомлення надсилається з того самого потоку, що і надіслав початкове повідомлення про блокування. Розблокування блокування з іншої нитки може призвести до невизначеної поведінки.



6

У сучасному Swift 5, з можливістю повернення:

/**
Makes sure no other thread reenters the closure before the one running has not returned
*/
@discardableResult
public func synchronized<T>(_ lock: AnyObject, closure:() -> T) -> T {
    objc_sync_enter(lock)
    defer { objc_sync_exit(lock) }

    return closure()
}

Використовуйте його так, щоб скористатися можливістю повернення:

let returnedValue = synchronized(self) { 
     // Your code here
     return yourCode()
}

Або так інакше:

synchronized(self) { 
     // Your code here
    yourCode()
}

2
Це правильна відповідь, а не прийнята та високо оцінена (що залежить від GCD). Здається, по суті ніхто не використовує і не розуміє, як їх використовувати Thread. Я задоволений цим - тоді як GCDце загрожує обмеженнями та обмеженнями.
javadba

4

Спробуйте: NSRecursiveLock

Блокування, яке може бути придбане одним і тим же потоком кілька разів, не викликаючи тупик.

let lock = NSRecursiveLock()

func f() {
    lock.lock()
    //Your Code
    lock.unlock()
}

func f2() {
    lock.lock()
    defer {
        lock.unlock()
    }
    //Your Code
}

2

На малюнку я опублікую свою реалізацію Swift 5, засновану на попередніх відповідях. Спасибі, хлопці! Мені було корисно мати той, який також повертає значення, тому у мене є два методи.

Ось простий клас, який потрібно зробити першим:

import Foundation
class Sync {
public class func synced(_ lock: Any, closure: () -> ()) {
        objc_sync_enter(lock)
        defer { objc_sync_exit(lock) }
        closure()
    }
    public class func syncedReturn(_ lock: Any, closure: () -> (Any?)) -> Any? {
        objc_sync_enter(lock)
        defer { objc_sync_exit(lock) }
        return closure()
    }
}

Потім використовуйте його так, якщо вам потрібно повернути значення:

return Sync.syncedReturn(self, closure: {
    // some code here
    return "hello world"
})

Або:

Sync.synced(self, closure: {
    // do some work synchronously
})

Спробуйте public class func synced<T>(_ lock: Any, closure: () -> T), працює для обох, недійсних та будь-якого іншого типу. Є також речі regrow.
hnh

@hnh, що ти маєш на увазі під матеріалами regrow? Крім того, якщо ви бажаєте поділитися прикладом виклику до загального методу з типом <T>, який допоможе мені оновити відповідь - мені подобається, куди ви з цим рухаєтесь.
TheJeff

rethrows, не regrow, srz
hnh

1

Деталі

xCode 8.3.1, швидкий 3.1

Завдання

Прочитати значення запису з різних потоків (async).

Код

class AsyncObject<T>:CustomStringConvertible {
    private var _value: T
    public private(set) var dispatchQueueName: String

    let dispatchQueue: DispatchQueue

    init (value: T, dispatchQueueName: String) {
        _value = value
        self.dispatchQueueName = dispatchQueueName
        dispatchQueue = DispatchQueue(label: dispatchQueueName)
    }

    func setValue(with closure: @escaping (_ currentValue: T)->(T) ) {
        dispatchQueue.sync { [weak self] in
            if let _self = self {
                _self._value = closure(_self._value)
            }
        }
    }

    func getValue(with closure: @escaping (_ currentValue: T)->() ) {
        dispatchQueue.sync { [weak self] in
            if let _self = self {
                closure(_self._value)
            }
        }
    }


    var value: T {
        get {
            return dispatchQueue.sync { _value }
        }

        set (newValue) {
            dispatchQueue.sync { _value = newValue }
        }
    }

    var description: String {
        return "\(_value)"
    }
}

Використання

print("Single read/write action")
// Use it when when you need to make single action
let obj = AsyncObject<Int>(value: 0, dispatchQueueName: "Dispatch0")
obj.value = 100
let x = obj.value
print(x)

print("Write action in block")
// Use it when when you need to make many action
obj.setValue{ (current) -> (Int) in
    let newValue = current*2
    print("previous: \(current), new: \(newValue)")
    return newValue
}

Повний зразок

розширення DispatchGroup

extension DispatchGroup {

    class func loop(repeatNumber: Int, action: @escaping (_ index: Int)->(), completion: @escaping ()->()) {
        let group = DispatchGroup()
        for index in 0...repeatNumber {
            group.enter()
            DispatchQueue.global(qos: .utility).async {
                action(index)
                group.leave()
            }
        }

        group.notify(queue: DispatchQueue.global(qos: .userInitiated)) {
            completion()
        }
    }
}

клас ViewController

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        //sample1()
        sample2()
    }

    func sample1() {
        print("=================================================\nsample with variable")

        let obj = AsyncObject<Int>(value: 0, dispatchQueueName: "Dispatch1")

        DispatchGroup.loop(repeatNumber: 5, action: { index in
            obj.value = index
        }) {
            print("\(obj.value)")
        }
    }

    func sample2() {
        print("\n=================================================\nsample with array")
        let arr = AsyncObject<[Int]>(value: [], dispatchQueueName: "Dispatch2")
        DispatchGroup.loop(repeatNumber: 15, action: { index in
            arr.setValue{ (current) -> ([Int]) in
                var array = current
                array.append(index*index)
                print("index: \(index), value \(array[array.count-1])")
                return array
            }
        }) {
            print("\(arr.value)")
        }
    }
}

1

Завдяки обгортці власності Swift це я зараз використовую:

@propertyWrapper public struct NCCSerialized<Wrapped> {
    private let queue = DispatchQueue(label: "com.nuclearcyborg.NCCSerialized_\(UUID().uuidString)")

    private var _wrappedValue: Wrapped
    public var wrappedValue: Wrapped {
        get { queue.sync { _wrappedValue } }
        set { queue.sync { _wrappedValue = newValue } }
    }

    public init(wrappedValue: Wrapped) {
        self._wrappedValue = wrappedValue
    }
}

Тоді ви можете просто зробити:

@NCCSerialized var foo: Int = 10

або

@NCCSerialized var myData: [SomeStruct] = []

Потім перейдіть до змінної, як зазвичай.


1
Мені подобається це рішення, але мені було цікаво про вартість людей @Decorating, оскільки це робить побічний ефект від створення DispatchQueueякого приховано від користувача. Я знайшов цю ТАКУ посилання, щоб полегшити свою думку: stackoverflow.com/a/35022486/1060314
Адам Вентурелла,

Сама упаковка властивості досить легка - просто структура, тож одна з найлегших речей, яку ви можете зробити. Дякуємо за посилання на DispatchQueue, хоча. Мені довелося зробити тестування продуктивності на обгортці queue.sync порівняно з іншими рішеннями (і проти черги немає), але цього не зробили.
drewster

1

На закінчення, тут наведемо більш поширений спосіб, який включає повернення значення або недійсність та кидання

import Foundation

extension NSObject {


    func synchronized<T>(lockObj: AnyObject!, closure: () throws -> T) rethrows ->  T
    {
        objc_sync_enter(lockObj)
        defer {
            objc_sync_exit(lockObj)
        }

        return try closure()
    }


}

0

Навіщо ускладнювати клопоти із замками? Використовуйте диспетчерські бар'єри.

Диспетчерський бар'єр створює точку синхронізації в паралельній черзі.

Поки він працює, жоден інший блок у черзі не дозволяється запускати, навіть якщо це паралельно та інші ядра доступні.

Якщо це звучить як ексклюзивний замок (запису), це так. Безбар'єрні блоки можна розглядати як спільні (зчитувані) замки.

Поки весь доступ до ресурсу здійснюється через чергу, бар'єри забезпечують дуже дешеву синхронізацію.


2
Я маю на увазі, ви припускаєте використання черги GCD для синхронізації доступу, але це не згадується в початковому запитанні. І бар'єр необхідний лише для паралельної черги - ви можете просто використовувати послідовну чергу для черги взаємовиключених блоків для емуляції блокування.
Білл

Моє запитання, навіщо емулювати замок? З того, що я читав, блокування не відволікається через накладні та бар'єри в черзі.
Frederick C. Lee

0

На основі "Євробур" перевірити випадок підкласу

class Foo: NSObject {
    func test() {
        print("1")
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
            print("3")
        }

        print("2")
    }
}


class Foo2: Foo {
    override func test() {
        super.test()

        print("11")
        objc_sync_enter(self)
        defer {
            print("33")
            objc_sync_exit(self)
        }

        print("22")
    }
}

let test = Foo2()
test.test()

Вихід:

1
2
3
11
22
33

0

dispatch_barrier_async - кращий спосіб, не блокуючи поточну нитку.

dispatch_barrier_async (accessQueue, {словник [object.ID] = об'єкт})


-5

Інший метод - створити суперклас і потім успадкувати його. Таким чином ви можете використовувати GCD більш безпосередньо

class Lockable {
    let lockableQ:dispatch_queue_t

    init() {
        lockableQ = dispatch_queue_create("com.blah.blah.\(self.dynamicType)", DISPATCH_QUEUE_SERIAL)
    }

    func lock(closure: () -> ()) {
        dispatch_sync(lockableQ, closure)
    }
}


class Foo: Lockable {

    func boo() {
        lock {
            ....... do something
        }
    }

10
-1 Спадщина дає поліморфізм підтипу взамін на посилення зв'язку. Уникайте пізнішого, якщо вам не потрібен перший. Не лінуйся. Віддайте перевагу композиції для повторного використання коду.
Яно
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.