web-dev-qa-db-fra.com

Ajouter des éléments à Swift array sur plusieurs threads, posant des problèmes (car les tableaux ne sont pas thread-safe) - comment puis-je contourner ce problème?

Je veux ajouter des blocs donnés à un tableau, puis exécuter tous les blocs contenus dans le tableau, lorsque demandé. J'ai un code similaire à ceci:

class MyArrayBlockClass {
    private var blocksArray: Array<() -> Void> = Array()

    private let blocksQueue: NSOperationQueue()

    func addBlockToArray(block: () -> Void) {
        self.blocksArray.append(block)
    }

    func runBlocksInArray() {
        for block in self.blocksArray {
            let operation = NSBlockOperation(block: block)
            self.blocksQueue.addOperation(operation)
        }

        self.blocksQueue.removeAll(keepCapacity: false)
    }
}

Le problème vient du fait que addBlockToArray peut être appelé sur plusieurs threads. Ce qui se passe, c'est que addBlockToArray est appelé en succession rapide sur différents threads et qu'il ne fait qu'ajouter l'un des éléments. Par conséquent, l'autre élément n'est pas appelé lors de l'exécution de runBlocksInArray.

J'ai essayé quelque chose comme ça, qui ne semble pas fonctionner:

private let blocksDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

func addBlockToArray(block: () -> Void) {
    dispatch_async(blocksDispatchQueue) {
        self.blocksArray.append(block)
    }
}
15
Andrew

Vous avez défini votre blocksDispatchQueue comme étant une file d'attente globale. En mettant à jour ceci pour Swift 3, l’équivalent est:

private let blocksDispatchQueue = DispatchQueue.global()

func addBlockToArray(block: @escaping () -> Void) {
    blocksDispatchQueue.async {
        self.blocksArray.append(block)
    }
}

Le problème est que les files d'attente globales sont des files d'attente simultanées. Par conséquent, vous n'obtenez pas la synchronisation souhaitée. Mais si vous aviez créé votre propre file d’attente en série, cela aurait été bien, par exemple. dans Swift 3:

private let blocksDispatchQueue = DispatchQueue(label: "com.domain.app.blocks")

Cette file d'attente personnalisée est, par défaut, une file d'attente série. Ainsi, vous obtiendrez la synchronisation souhaitée.

Remarque: si vous utilisez cette blocksDispatchQueue pour synchroniser votre interaction avec cette file d'attente, les interactions all avec cette blocksArray doivent être coordonnées via cette file d'attente, par exemple. envoyez également le code pour ajouter les opérations à l'aide de la même file d'attente:

func runBlocksInArray() {
    blocksDispatchQueue.async {
        for block in self.blocksArray {
            let operation = BlockOperation(block: block)
            self.blocksQueue.addOperation(operation)
        }

        self.blocksArray.removeAll()
    }
}

Vous pouvez également utiliser le modèle lecteur/graveur pour créer votre propre file d'attente simultanée:

private let blocksDispatchQueue = DispatchQueue(label: "com.domain.app.blocks", attributes: .concurrent)

Mais dans le modèle lecteur-écrivain, les écritures doivent être effectuées en utilisant une barrière (obtenant un comportement de type série pour les écritures):

func addBlockToArray(block: @escaping () -> Void) {
    blocksDispatchQueue.async(flags: .barrier) {
        self.blocksArray.append(block)
    }
}

Mais vous pouvez maintenant lire les données, comme ci-dessus:

blocksDispatchQueue.sync {
    someVariable = self.blocksArray[index]
}

L'avantage de ce modèle est que les écritures sont synchronisées, mais que les lectures peuvent avoir lieu simultanément l'une par rapport à l'autre. Ce n'est probablement pas critique dans ce cas (une simple file d'attente sérielle serait probablement suffisante), mais j'inclus ce motif de lecture/écriture pour des raisons de complétude.

Si vous recherchez des exemples de Swift 2, voyez le rendu précédent de cette réponse.

23
Rob

Pour la synchronisation entre les threads, utilisez dispatch_sync (pas _async) et votre propre file d'attente de distribution (pas la file globale): 

class MyArrayBlockClass {
    private var queue = dispatch_queue_create("andrew.myblockarrayclass", nil)

    func addBlockToArray(block: () -> Void) {
        dispatch_sync(queue) {
            self.blocksArray.append(block)
        } 
    }
    //....
}

dispatch_sync est agréable et facile à utiliser et devrait suffire à votre cas (je l’utilise pour tous mes besoins de synchronisation de threads pour le moment), mais vous pouvez également utiliser des verrous et des mutex de niveau inférieur. Mike Ash a rédigé un excellent article sur différents choix: Serrures, sécurité du fil et Swift

3
Teemu Kurppa

Créez une file d'attente série et apportez des modifications au tableau dans ce thread. Votre appel de création de thread devrait ressembler à ceci: 

private let blocksDispatchQueue = dispatch_queue_create("SynchronizedArrayAccess", DISPATCH_QUEUE_SERIAL)

Ensuite, vous pouvez l'utiliser de la même manière que maintenant.

func addBlockToArray(block: () -> Void) {
    dispatch_async(blocksDispatchQueue) {
        self.blocksArray.append(block)
    }
}
1
Sumeet

Détails

  • Xcode 10.1 (10B61)
  • Swift 4.2

Solution

import Foundation

class AtomicArray<T> {

    private lazy var semaphore = DispatchSemaphore(value: 1)
    private var array: [T]

    init (array: [T]) { self.array = array }

    func append(newElement: T) {
        wait(); defer { signal() }
        array.append(newElement)
    }

    subscript(index: Int) -> T {
        get {
            wait(); defer { signal() }
            return array[index]
        }
        set(newValue) {
            wait(); defer { signal() }
            array[index] = newValue
        }
    }

    var count: Int {
        wait(); defer { signal() }
        return array.count
    }

    private func wait() { semaphore.wait() }
    private func signal() { semaphore.signal() }

    func set(closure: (_ curentArray: [T])->([T]) ) {
        wait(); defer { signal() }
        array = closure(array)
    }

    func get(closure: (_ curentArray: [T])->()) {
        wait(); defer { signal() }
        closure(array)
    }

    func get() -> [T] {
        wait(); defer { signal() }
        return array
    }
}

extension AtomicArray: CustomStringConvertible {
    var description: String { return "\(get())"}
}

Usage

L'idée de base est d'utiliser la syntaxe d'un tableau régulier

let atomicArray = AtomicArray(array: [3,2,1])

 print(atomicArray)
 atomicArray.append(newElement: 1)

 let arr = atomicArray.get()
 print(arr)
 atomicArray[2] = 0

 atomicArray.get { currentArray in
      print(currentArray)
 }

 atomicArray.set { currentArray -> [Int] in
      return currentArray.map{ item -> Int in
           return item*item
      }
 }
 print(atomicArray)

Résultat d'utilisation

 enter image description here

Échantillon complet

import UIKit

class ViewController: UIViewController {

    var atomicArray = AtomicArray(array: [Int](repeating: 0, count: 100))

    let dispatchGroup = DispatchGroup()

    override func viewDidLoad() {
        super.viewDidLoad()

        arrayInfo()

        sample { index, dispatch in
            self.atomicArray[index] += 1
        }

        dispatchGroup.notify(queue: .main) {
            self.arrayInfo()
            self.atomicArray.set { currentArray -> ([Int]) in
                return currentArray.map{ (item) -> Int in
                    return item + 100
                }
            }
           self.arrayInfo()
        }

    }

    private func arrayInfo() {
        print("Count: \(self.atomicArray.count)\nData: \(self.atomicArray)")
    }

    func sample(closure: @escaping (Int,DispatchQueue)->()) {

        print("----------------------------------------------\n")

        async(dispatch: .main, closure: closure)
        async(dispatch: .global(qos: .userInitiated), closure: closure)
        async(dispatch: .global(qos: .utility), closure: closure)
        async(dispatch: .global(qos: .default), closure: closure)
        async(dispatch: .global(qos: .userInteractive), closure: closure)
    }

    private func async(dispatch: DispatchQueue, closure: @escaping (Int,DispatchQueue)->()) {

        for index in 0..<atomicArray.count {
            dispatchGroup.enter()
            dispatch.async {
                closure(index,dispatch)
                self.dispatchGroup.leave()
            }
        }
    }
}

Résultat complet de l'échantillon

 enter image description here

0
Vasily Bodnarchuk

NSOperationQueue lui-même est thread-safe, vous pouvez donc définir suspended sur true, ajouter tous les blocs souhaités à partir de n'importe quel thread, puis définir suspended sur false pour exécuter tous les blocs.

0
Jack Lawrence