Qual é o Swift equivalente ao "@synchronized" do Objective-C?


231

Pesquisei o livro Swift, mas não consigo encontrar a versão Swift de @synchronized. Como faço para exclusão mútua no Swift?


1
Eu usaria uma barreira de despacho. Barreiras fornecem sincronização muito barata. dispatch_barrier_async (). etc
Frederick C. Lee

@ FrederickC.Lee, e se você precisar que uma gravação seja sincronizada, como ao criar um wrapper removeFirst()?
ScottyBlades

Respostas:


183

Você pode usar o GCD. É um pouco mais detalhado do que @synchronized, mas funciona como um substituto:

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

12
Isso é ótimo, mas falta a capacidade de reentrada que você possui com o @synchronized.
Michael Waterfall

9
Com essa abordagem, você precisa ter cuidado. Seu bloco pode ser executado em algum outro encadeamento. Os documentos da API dizem: "Como uma otimização, essa função chama o bloco no thread atual quando possível."
bio

20
Excelente artigo de Matt Gallagher sobre isso: cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html
wuf810

4
Não, isso causa impasses ocasionais.
Tom Kraina

70
Não, não e não. Boa tentativa, mas funciona imperfeitamente bem. Por quê? A leitura essencial (comparação abrangente de alternativas, cuidados) e uma excelente estrutura de utilidade de Matt Gallagher, aqui: cocoawithlove.com/blog/2016/06/02/threads-and-mutexes.html @ wuf810 mencionaram isso primeiro (HT), mas subestimado quão bom é este artigo. Todos devem ler. (Por favor upvote este pelo mínimo para torná-lo visível inicialmente, mas não mais.)
t0rst

181

Eu mesmo estava procurando isso e cheguei à conclusão de que ainda não há nenhuma construção nativa dentro do swift.

Eu criei essa pequena função auxiliar com base em alguns dos códigos que eu vi de Matt Bridges e outros.

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

O uso é bastante direto

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

Há um problema que encontrei com isso. Passar em uma matriz como o argumento de bloqueio parece causar um erro muito obtuso do compilador neste momento. Caso contrário, embora pareça funcionar como desejado.

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!

Agradável! Por favor, registre um bug para isso, se ainda houver um problema na
versão

14
Isso é bastante útil e preserva bem a sintaxe do @synchronizedbloco, mas observe que ele não é idêntico a uma instrução de bloco embutida real como o @synchronizedbloco no Objective-C, porque as instruções returne breaknão funcionam mais para pular da função / loop circundante como seria se fosse uma afirmação comum.
Newacct 11/10

3
O erro é provavelmente devido a que as matrizes são transmitidos como valores não referências
james_alvarez

9
Provavelmente, esse seria um ótimo lugar para usar a nova deferpalavra-chave para garantir que objc_sync_exitseja chamado mesmo que seja closurelançado.
precisa saber é o seguinte

3
@ t0rst Chamar esta resposta de "falha" com base no artigo vinculado não é válida. O artigo diz que esse método é "um pouco mais lento que o ideal" e "está limitado às plataformas da Apple". Isso não o torna "falho" por um longo tempo.
RenniePet

151

Gosto e uso muitas das respostas aqui, portanto, escolho o que for melhor para você. Dito isso, o método que eu prefiro quando preciso de algo como o objetivo-c @synchronizedusa a deferinstrução introduzida no swift 2.

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

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

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

A coisa agradável sobre este método, é que a sua seção crítica pode sair do bloco contendo de qualquer forma desejada (por exemplo, return, break, continue, throw), e "as declarações dentro a declaração de adiamento são executados não importa como o controle do programa é transferido." 1


Eu acho que essa é provavelmente a solução mais elegante fornecida aqui. Obrigado pelo seu feedback!
Scott D

3
O que é lock? Como é lockinicializado?
Van Du Tran

6
locké qualquer objeto objetivo-c.
precisa saber é o seguinte

1
Excelente! Eu havia escrito alguns métodos auxiliares de bloqueio quando o Swift 1 foi introduzido e não os havia revisado há algum tempo. Esqueci completamente o adiamento; este é o caminho a seguir!
Randy

Eu gosto disso, mas recebo um erro do compilador "O bloco de instruções agrupado é um fechamento não utilizado" no Xcode 8. Ah, entendo, eles são apenas as chaves de função - muito tempo para encontrar o link de referência "1" - obrigado!
Duncan Groenewald #

83

Você pode colocar declarações entre objc_sync_enter(obj: AnyObject?)e objc_sync_exit(obj: AnyObject?). A palavra-chave @synchronized está usando esses métodos nos bastidores. ie

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

3
Isso será considerado uso de uma API privada pela Apple?
Drux 27/01

2
Não, objc_sync_entere objc_sync_exitos métodos são definidos no Objc-sync.h e são de código aberto: opensource.apple.com/source/objc4/objc4-371.2/runtime/…
bontoJR

O que acontece se vários encadeamentos tentam acessar o mesmo recurso, o segundo espera, tenta novamente ou falha?
precisa saber é o seguinte

Adicionando ao que o @bontoJR disse, objc_sync_enter(…)e objc_sync_exit(…)são cabeçalhos públicos fornecidos pelo iOS / macOS / etc. APIs (parece que elas estão dentro ….sdkdo caminho usr/include/objc/objc-sync.h) . A maneira mais fácil de descobrir se algo é uma API pública ou não é (no Xcode) digitar o nome da função (por exemplo objc_sync_enter(), os argumentos não precisam ser especificados para as funções C) e , em seguida, clique com o botão direito do mouse. Se ele mostrar o arquivo de cabeçalho para essa API, você será bom (já que não conseguiria ver o cabeçalho se não fosse público) .
Slipp D. Thompson

75

O análogo da @synchronizeddiretiva do Objective-C pode ter um tipo de retorno arbitrário e um bom rethrowscomportamento no 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()
}

O uso da deferinstrução permite retornar diretamente um valor sem introduzir uma variável temporária.


No Swift 2, adicione o @noescapeatributo ao fechamento para permitir mais otimizações:

// 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()
}

Com base nas respostas de GNewc [1] (onde eu gosto do tipo de retorno arbitrário) e Tod Cunningham [2] (onde eu gosto defer).


Xcode está me dizendo que @noescape agora é padrão e é depreciado em Swift 3.
RenniePet

É isso mesmo, o código nesta resposta é para o Swift 2 e requer alguma adaptação para o Swift 3. Vou atualizá-lo quando tiver tempo.
Wdiveriver 12/12

1
Você pode explicar o uso? Talvez com um exemplo .. obrigado antecipadamente! No meu caso, eu tenho um conjunto que preciso sincronizar, porque manipulo seu conteúdo em um DispatchQueue.
Sancho

@ sancho Eu prefiro manter este post conciso. Você parece perguntar sobre diretrizes gerais de programação simultânea, essa é uma pergunta ampla. Tente fazer isso como uma pergunta separada!
#

41

SWIFT 4

No Swift 4, você pode usar as filas de despacho GCDs para bloquear recursos.

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

Isso não parece funcionar com o XCode8.1. .serialparece estar indisponível. Mas .concurrentestá disponível. : /
Travis Griggs

2
o padrão é .serial
Duncan Groenewald

2
Observe que esse padrão não protege adequadamente contra os problemas mais comuns de multithread. Por exemplo, se você executasse myObject.state = myObject.state + 1simultaneamente, não contaria o total de operações, mas produziria um valor não determinístico. Para resolver esse problema, o código de chamada deve ser agrupado em uma fila serial para que a leitura e a gravação ocorram atomicamente. É claro que os Obj-c @synchronisedtêm o mesmo problema, portanto, nesse sentido, sua implementação está correta.
22718 Berik

1
Sim, myObject.state += 1é uma combinação de uma operação de leitura e gravação. Outro segmento ainda pode estar entre eles para definir / escrever um valor. Conforme objc.io/blog/2018/12/18/atomic-variables , seria mais fácil executar o setbloqueio / fechamento em sincronização e não a variável em si.
CyberMew

24

Usando a resposta de Bryan McLemore, eu a estendi para apoiar objetos que oferecem uma mansão segura com a capacidade de adiamento do Swift 2.0.

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

    try block()
}

Seria melhor usar rethrowspara simplificar o uso com fechamentos sem arremesso (sem necessidade de usar try), conforme mostrado na minha resposta .
Wdiveriver

23

Para adicionar funcionalidade de retorno, você pode fazer o seguinte:

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

Posteriormente, você pode chamá-lo usando:

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

10

Swift 3

Este código tem a capacidade de reentrada e pode funcionar com chamadas de função assíncronas. Nesse código, depois que someAsyncFunc () é chamado, outro fechamento de função na fila serial será processado, mas será bloqueado pelo semáforo.wait () até o sinal () ser chamado. internalQueue.sync não deve ser usado, pois ele bloqueará o thread principal se não me engano.

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 não é uma boa ideia sem tratamento de erros.


Qual tratamento de erro? O compilador não permitirá nada que seja lançado. Por outro lado, ao não usar objc_sync_enter / exit, você desiste de alguns ganhos substanciais de desempenho.
gnasher729

8

Na sessão "Compreendendo falhas e logs de falhas" 414 da WWDC 2018, eles mostram a seguinte maneira, usando DispatchQueues com sincronização.

No swift 4 deve ser algo como o seguinte:

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

De qualquer forma, você também pode fazer leituras mais rapidamente usando filas simultâneas com barreiras. As leituras de sincronização e assíncrona são executadas simultaneamente e a gravação de um novo valor aguarda o término das operações anteriores.

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

você provavelmente não precisa bloquear leituras e desacelerar a fila usando a sincronização. Você pode apenas usar a sincronização para gravação em série.
Basheer_CAD 11/09/18

6

Use o NSLock no 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()

Aviso A classe NSLock usa threads POSIX para implementar seu comportamento de bloqueio. Ao enviar uma mensagem de desbloqueio para um objeto NSLock, você deve ter certeza de que a mensagem é enviada do mesmo encadeamento que enviou a mensagem de bloqueio inicial. Desbloquear um bloqueio de um thread diferente pode resultar em um comportamento indefinido.



6

No Swift 5 moderno, com capacidade de retorno:

/**
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()
}

Use-o assim, para aproveitar o recurso de valor de retorno:

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

Ou assim:

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

2
Essa é a resposta correta, e não a aceita e altamente votada (da qual depende GCD). Parece que essencialmente ninguém usa ou entende como usar Thread. Estou muito feliz com isso - ao passo que GCDestá cheio de truques e limitações.
javadba

4

Experimente: NSRecursiveLock

Um bloqueio que pode ser adquirido várias vezes pelo mesmo encadeamento sem causar um conflito.

let lock = NSRecursiveLock()

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

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

2

Figura Vou postar minha implementação do Swift 5, construída com base nas respostas anteriores. Obrigado rapazes! Achei útil ter um que retorne um valor também, então eu tenho dois métodos.

Aqui está uma classe simples para fazer primeiro:

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

Em seguida, use-o assim se precisar de um valor de retorno:

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

Ou:

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

Tente public class func synced<T>(_ lock: Any, closure: () -> T), funciona para ambos, nulo e qualquer outro tipo. Há também o material regrows.
hnh

@hnh, o que você quer dizer com regrows? Além disso, se você estiver disposto a compartilhar uma chamada de exemplo para o método genérico com o tipo <T> que me ajudaria a atualizar a resposta - eu gosto de onde você está indo com isso.
TheJeff

repetições, não regrows, srz
hnh

1

Detalhes

xCode 8.3.1, swift 3.1

Tarefa

Leia o valor de gravação de diferentes segmentos (assíncrono).

Código

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

Uso

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
}

Amostra Completa

extensão 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()
        }
    }
}

classe 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

Com os wrappers de propriedades do Swift, é isso que estou usando agora:

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

Então você pode simplesmente fazer:

@NCCSerialized var foo: Int = 10

ou

@NCCSerialized var myData: [SomeStruct] = []

Em seguida, acesse a variável como faria normalmente.


1
Eu gosto dessa solução, mas estava curioso sobre o custo do pessoal @Decorating, pois fazer isso tem o efeito colateral de criar um DispatchQueueque está oculto para o usuário. Encontrei esta referência SO para tranqüilizar: stackoverflow.com/a/35022486/1060314
Adam Venturella

O wrapper de propriedade em si é bastante leve - apenas uma estrutura, portanto, uma das coisas mais leves que você pode fazer. Obrigado pelo link no DispatchQueue. Eu estava pensando em fazer alguns testes de desempenho na quebra da fila.sync versus outras soluções (e contra nenhuma fila), mas não o fiz.
drewster

1

Em conclusão, aqui é apresentada uma maneira mais comum que inclui valor de retorno ou nulo e

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

Por que dificultar e aborrecer os bloqueios? Use Barreiras de Despacho.

Uma barreira de despacho cria um ponto de sincronização dentro de uma fila simultânea.

Enquanto estiver em execução, nenhum outro bloco na fila poderá executar, mesmo que seja concorrente e outros núcleos estejam disponíveis.

Se isso soa como um bloqueio exclusivo (gravação), é. Blocos sem barreira podem ser considerados bloqueios compartilhados (lidos).

Desde que todo o acesso ao recurso seja realizado através da fila, as barreiras fornecem uma sincronização muito barata.


2
Quero dizer, você está assumindo o uso de uma fila GCD para sincronizar o acesso, mas isso não é mencionado na pergunta original. E uma barreira só é necessária com uma fila simultânea - você pode simplesmente usar uma fila serial para enfileirar blocos excluídos mutuamente para emular um bloqueio.
Bill

Minha pergunta, por que imitar uma fechadura? Pelo que li, os bloqueios são desencorajados devido à sobrecarga versus uma barreira dentro de uma fila.
Frederick C. Lee

0

Com base no obeuroburɳ , teste um caso de subclasse

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()

Resultado:

1
2
3
11
22
33

0

dispatch_barrier_async é a melhor maneira, sem bloquear o segmento atual.

dispatch_barrier_async (accessQueue, {dictionary [object.ID] = object})


-5

Outro método é criar uma superclasse e depois herdá-la. Dessa forma, você pode usar o GCD mais diretamente

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
A herança -1 fornece polimorfismo de subtipo em troca do aumento do acoplamento. Evite o mais tarde se você não precisar do primeiro. Não seja preguiçoso. Prefira composição para reutilização de código.
Jano
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.