Что такое Swift, эквивалентный «@synchronized» Objective-C?


231

Я искал книгу Swift, но не могу найти версию @synchronized Swift. Как сделать взаимное исключение в 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
Нет, это вызывает случайные тупики.
Том Крайна

71
Нет, нет и нет. Хорошая попытка, но работает не очень хорошо. Зачем? Основное чтение (всестороннее сравнение альтернатив, предостережения) и отличный полезный фреймворк от Мэтта Галлахера, здесь: cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html @ wuf810 упомянул об этом первым (HT), но недооценил, насколько хороша эта статья. Все должны прочитать. (Пожалуйста, подтвердите это минимумом, чтобы сделать его изначально видимым, но не более.)
Первое

181

Я сам искал это и пришел к выводу, что внутри swift пока нет нативной конструкции.

Я сделал эту маленькую вспомогательную функцию на основе некоторого кода, который я видел от Мэтта Бриджеса и других.

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вызов будет вызван, даже если closurethrows.
devios1

3
@ t0rst Называть этот ответ «ошибочным» на основании ссылки на статью недействительно. В статье говорится, что этот метод «немного медленнее идеального» и «ограничен платформами Apple». Это не делает его "ущербным" в конечном итоге.
RenniePet

151

Мне нравятся и используются многие ответы здесь, поэтому я бы выбрал тот, который подходит вам лучше всего. Тем не менее, метод, который я предпочитаю, когда мне нужно что-то вроде target-c, @synchronizedиспользует deferоператор, введенный в swift 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инициализируется?
Ван Ду Тран

6
lockэто любой объект-объект.
uroeuroburɳ

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 и с открытым исходным кодом
bontoJR

Что произойдет, если несколько потоков попытаются получить доступ к одному и тому же ресурсу, второй ожидает, повторяется или происходит сбой?
TruMan1

Добавление на то , что сказал @bontoJR, objc_sync_enter(…)и objc_sync_exit(…)являются открытыми заголовки , предоставляемые КСН / 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
Дункан Гроенвальд

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

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

24

Используя ответ Брайана МакЛемора, я расширил его, чтобы он поддерживал объекты, которые бросают в безопасное поместье со способностью отсрочки 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

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
  }
}

10

Свифт 3

Этот код имеет возможность повторного ввода и может работать с асинхронными вызовами функций. В этом коде после вызова someAsyncFunc () другая функция закрытия в последовательной очереди будет обрабатываться, но будет блокироваться semaphore.wait () до вызова signal (). innerQueue.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 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как чревато недостатками и ограничениями.
Джавадба

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), работает как для void, так и для любого другого типа. Существует также материал для роста.
HNH

@hnh, что ты имеешь в виду, когда говоришь о ростках? Также, если вы захотите поделиться примером вызова универсального метода с типом <T>, который поможет мне обновить ответ - мне нравится, куда вы идете с этим.
TheJeff

бросает, а не снова, srz
хн

1

подробности

xCode 8.3.1, swift 3.1

задача

Чтение значения записи из разных потоков (асинхронное).

Код

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 по сравнению с другими решениями (и без очереди), но я этого не сделал.
Дрюстер

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 для синхронизации доступа, но это не упоминалось в первоначальном вопросе. И барьер необходим только для параллельной очереди - вы можете просто использовать последовательную очередь, чтобы поставить в очередь взаимно исключенные блоки для эмуляции блокировки.
Билл

Мой вопрос, зачем эмулировать блокировку? Из того, что я прочитал, блокировки не приветствуются из-за накладных расходов по сравнению с барьером в очереди.
Фредерик С. Ли,

0

На основе «eurobur» , протестируйте случай подкласса

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.