Создать потокобезопасный массив в Swift
У меня проблема с потоками в Swift. У меня есть массив с некоторыми объектами в нем. Через делегата класс получает новые объекты примерно каждую секунду. После этого я должен проверить, есть ли объекты в массиве, поэтому я должен обновить объект, в противном случае я должен удалить / добавить новый объект.
Если я добавляю новый объект, мне сначала нужно получить некоторые данные по сети. Это гендельт через блок.
Теперь моя проблема в том, как мне синхронизировать эту задачу?
Я пробовал dispatch_semaphore, но этот блокирует пользовательский интерфейс, пока блок не будет завершен.
Я также попробовал простую переменную bool, которая проверяет, выполняется ли блок в настоящий момент, и тем временем пропускает метод сравнения.
Но оба метода не идеальны.
Что является лучшим способом для управления массивом, я не хочу дублировать данные в массиве.
16 ответов
Kirsteins верен, но вам не всегда нужно использовать очередь отправки. Ты можешь использовать:
objc_sync_enter(array)
// manipulate the array
objc_sync_exit(array)
Это должно сработать. Для дополнительного бонуса вы можете создать функцию, которая будет использоваться всякий раз, когда вам нужна безопасность потоков:
func sync(lock: NSObject, closure: () -> Void) {
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
}
...
var list = NSMutableArray()
sync (list) {
list.addObject("something")
}
Обратите внимание, что я изменился AnyObject
в NSObject
, В коллекции Swift типы реализованы как struct
s и они передаются по значению, поэтому я предполагаю, что было бы безопаснее работать с изменяемыми классами коллекций, которые передаются по ссылке при использовании удобного sync
функция.
Обновление для Swift
Рекомендуемый шаблон для многопоточного доступа использует диспетчеризацию barrier
:
let queue = DispatchQueue(label: "thread-safe-obj", attributes: .concurrent)
// write
queue.async(flags: .barrier) {
// perform writes on data
}
// read
var value: ValueType!
queue.sync {
// perform read and assign value
}
return value
Я не знаю, почему люди применяют такие сложные подходы к такой простой вещи
Не злоупотребляйте
DispatchQueues
для блокировки. С помощьюqueue.sync
это не что иное, как получение блокировки и передача работы другому потоку, пока блокировка (DispatchGroup
) ждет. Это не только не нужно, но и может иметь побочные эффекты в зависимости от того, что вы блокируете. Вы можете сами найти это в Источнике GCD.Не использовать
objc_sync_enter/exit
, они используются ObjCs@synchronized
который будет неявно связывать коллекции Swift с аналогом ObjC, что также не нужно. И это устаревший API.
Просто установите блокировку и защитите доступ к своей коллекции.
var lock = NSLock()
var a = [1, 2, 3]
lock.lock()
a.append(4)
lock.unlock()
Если вы хотите немного упростить себе жизнь, определите небольшое расширение для методов, которые вам нужно заблокировать.
extension Array {
mutating func append(_ newElement: Element, _ lock: inout NSLock) {
lock.lock()
defer { lock.unlock() }
append(newElement)
}
}
var lock = NSLock()
var a = [1, 2, 3]
a.append(4, &lock)
Не требуется переключение контекста, и это даже проще, чем обернуть все в sync
блок. Или используйте расширение блокировки для большей гибкости
extension NSLock {
@discardableResult
func with<T>(_ block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}
let lock = NSLock()
var a = [1, 2, 3]
lock.with { a.append(4) }
Ответ Кирстейнса верен, но для удобства я обновил этот ответ, предложив Амола Чаудхари и Роба об использовании параллельной очереди с асинхронным барьером, чтобы разрешить одновременное чтение, но блокировать запись.
Я также обернул некоторые другие функции массива, которые были полезны для меня.
public class SynchronizedArray<T> {
private var array: [T] = []
private let accessQueue = dispatch_queue_create("SynchronizedArrayAccess", DISPATCH_QUEUE_CONCURRENT)
public func append(newElement: T) {
dispatch_barrier_async(self.accessQueue) {
self.array.append(newElement)
}
}
public func removeAtIndex(index: Int) {
dispatch_barrier_async(self.accessQueue) {
self.array.removeAtIndex(index)
}
}
public var count: Int {
var count = 0
dispatch_sync(self.accessQueue) {
count = self.array.count
}
return count
}
public func first() -> T? {
var element: T?
dispatch_sync(self.accessQueue) {
if !self.array.isEmpty {
element = self.array[0]
}
}
return element
}
public subscript(index: Int) -> T {
set {
dispatch_barrier_async(self.accessQueue) {
self.array[index] = newValue
}
}
get {
var element: T!
dispatch_sync(self.accessQueue) {
element = self.array[index]
}
return element
}
}
}
ОБНОВЛЕНИЕ Это тот же код, обновленный для Swift3.
public class SynchronizedArray<T> {
private var array: [T] = []
private let accessQueue = DispatchQueue(label: "SynchronizedArrayAccess", attributes: .concurrent)
public func append(newElement: T) {
self.accessQueue.async(flags:.barrier) {
self.array.append(newElement)
}
}
public func removeAtIndex(index: Int) {
self.accessQueue.async(flags:.barrier) {
self.array.remove(at: index)
}
}
public var count: Int {
var count = 0
self.accessQueue.sync {
count = self.array.count
}
return count
}
public func first() -> T? {
var element: T?
self.accessQueue.sync {
if !self.array.isEmpty {
element = self.array[0]
}
}
return element
}
public subscript(index: Int) -> T {
set {
self.accessQueue.async(flags:.barrier) {
self.array[index] = newValue
}
}
get {
var element: T!
self.accessQueue.sync {
element = self.array[index]
}
return element
}
}
}
Мой подход к этой проблеме заключался в использовании очереди последовательной отправки для синхронизации доступа к штучному массиву. Он блокирует поток, когда вы пытаетесь получить значение по индексу, а очередь действительно занята, но это проблема и с блокировками.
public class SynchronizedArray<T> {
private var array: [T] = []
private let accessQueue = dispatch_queue_create("SynchronizedArrayAccess", DISPATCH_QUEUE_SERIAL)
public func append(newElement: T) {
dispatch_async(self.accessQueue) {
self.array.append(newElement)
}
}
public subscript(index: Int) -> T {
set {
dispatch_async(self.accessQueue) {
self.array[index] = newValue
}
}
get {
var element: T!
dispatch_sync(self.accessQueue) {
element = self.array[index]
}
return element
}
}
}
var a = SynchronizedArray<Int>()
a.append(1)
a.append(2)
a.append(3)
// can be empty as this is non-thread safe access
println(a.array)
// thread-safe synchonized access
println(a[0])
println(a[1])
println(a[2])
подробности
- Xcode 10.1 (10B61)
- Swift 4.2
Решение
import Foundation
// https://developer.apple.com/documentation/swift/rangereplaceablecollection
struct AtomicArray<T: Equatable>: RangeReplaceableCollection {
typealias Element = T
typealias Index = Int
typealias SubSequence = AtomicArray<T>
typealias Indices = Range<Int>
fileprivate var array: Array<T>
private var arraySemaphore: DispatchSemaphore
var startIndex: Int { return array.startIndex }
var endIndex: Int { return array.endIndex }
var indices: Range<Int> { return array.indices }
func index(after i: Int) -> Int { return array.index(after: i) }
fileprivate func _wait() { arraySemaphore.wait() }
fileprivate func _signal() { arraySemaphore.signal() }
}
// Instance Methods
extension AtomicArray {
init<S>(_ elements: S) where S : Sequence, AtomicArray.Element == S.Element {
array = Array<S.Element>(elements)
arraySemaphore = DispatchSemaphore(value: 1)
}
init() { self.init([]) }
init(repeating repeatedValue: AtomicArray.Element, count: Int) {
let array = Array(repeating: repeatedValue, count: count)
self.init(array)
}
}
// Instance Methods
extension AtomicArray {
public mutating func append(_ newElement: AtomicArray.Element) {
_wait(); defer { _signal() }
array.append(newElement)
}
public mutating func append<S>(contentsOf newElements: S) where S : Sequence, AtomicArray.Element == S.Element {
_wait(); defer { _signal() }
array.append(contentsOf: newElements)
}
func filter(_ isIncluded: (AtomicArray.Element) throws -> Bool) rethrows -> AtomicArray {
_wait(); defer { _signal() }
let subArray = try array.filter(isIncluded)
return AtomicArray(subArray)
}
public mutating func insert(_ newElement: AtomicArray.Element, at i: AtomicArray.Index) {
_wait(); defer { _signal() }
array.insert(newElement, at: i)
}
mutating func insert<S>(contentsOf newElements: S, at i: AtomicArray.Index) where S : Collection, AtomicArray.Element == S.Element {
_wait(); defer { _signal() }
array.insert(contentsOf: newElements, at: i)
}
mutating func popLast() -> AtomicArray.Element? {
_wait(); defer { _signal() }
return array.popLast()
}
@discardableResult mutating func remove(at i: AtomicArray.Index) -> AtomicArray.Element {
_wait(); defer { _signal() }
return array.remove(at: i)
}
mutating func removeAll(keepingCapacity keepCapacity: Bool) {
_wait(); defer { _signal() }
array.removeAll()
}
mutating func removeAll(where shouldBeRemoved: (AtomicArray.Element) throws -> Bool) rethrows {
_wait(); defer { _signal() }
try array.removeAll(where: shouldBeRemoved)
}
@discardableResult mutating func removeFirst() -> AtomicArray.Element {
_wait(); defer { _signal() }
return array.removeFirst()
}
mutating func removeFirst(_ k: Int) {
_wait(); defer { _signal() }
array.removeFirst(k)
}
@discardableResult mutating func removeLast() -> AtomicArray.Element {
_wait(); defer { _signal() }
return array.removeLast()
}
mutating func removeLast(_ k: Int) {
_wait(); defer { _signal() }
array.removeLast(k)
}
mutating func removeSubrange(_ bounds: Range<Int>) {
_wait(); defer { _signal() }
array.removeSubrange(bounds)
}
mutating func replaceSubrange<C, R>(_ subrange: R, with newElements: C) where C : Collection, R : RangeExpression, T == C.Element, AtomicArray<T>.Index == R.Bound {
_wait(); defer { _signal() }
array.replaceSubrange(subrange, with: newElements)
}
mutating func reserveCapacity(_ n: Int) {
_wait(); defer { _signal() }
array.reserveCapacity(n)
}
public var count: Int {
_wait(); defer { _signal() }
return array.count
}
}
// Get/Set
extension AtomicArray {
// Single action
func get() -> [T] {
_wait(); defer { _signal() }
return array
}
mutating func set(array: [T]) {
_wait(); defer { _signal() }
self.array = array
}
// Multy actions
mutating func get(closure: ([T])->()) {
_wait(); defer { _signal() }
closure(array)
}
mutating func set(closure: ([T])->([T])) {
_wait(); defer { _signal() }
array = closure(array)
}
}
// Subscripts
extension AtomicArray {
subscript(bounds: Range<AtomicArray.Index>) -> AtomicArray.SubSequence {
get {
_wait(); defer { _signal() }
return AtomicArray(array[bounds])
}
}
subscript(bounds: AtomicArray.Index) -> AtomicArray.Element {
get {
_wait(); defer { _signal() }
return array[bounds]
}
set(value) {
_wait(); defer { _signal() }
array[bounds] = value
}
}
}
// Operator Functions
extension AtomicArray {
static func + <Other>(lhs: Other, rhs: AtomicArray) -> AtomicArray where Other : Sequence, AtomicArray.Element == Other.Element {
return AtomicArray(lhs + rhs.get())
}
static func + <Other>(lhs: AtomicArray, rhs: Other) -> AtomicArray where Other : Sequence, AtomicArray.Element == Other.Element{
return AtomicArray(lhs.get() + rhs)
}
static func + <Other>(lhs: AtomicArray, rhs: Other) -> AtomicArray where Other : RangeReplaceableCollection, AtomicArray.Element == Other.Element {
return AtomicArray(lhs.get() + rhs)
}
static func + (lhs: AtomicArray<T>, rhs: AtomicArray<T>) -> AtomicArray {
return AtomicArray(lhs.get() + rhs.get())
}
static func += <Other>(lhs: inout AtomicArray, rhs: Other) where Other : Sequence, AtomicArray.Element == Other.Element {
lhs._wait(); defer { lhs._signal() }
lhs.array += rhs
}
}
extension AtomicArray: Equatable {
static func == (lhs: AtomicArray<T>, rhs: AtomicArray<T>) -> Bool {
lhs._wait(); defer { lhs._signal() }
rhs._wait(); defer { rhs._signal() }
return lhs.array == rhs.array
}
}
extension AtomicArray: CustomStringConvertible {
var description: String {
_wait(); defer { _signal() }
return "\(array)"
}
}
Пример использования 1
import Foundation
// init
var array = AtomicArray<Int>()
print(array)
array = AtomicArray(repeating: 0, count: 5)
print(array)
array = AtomicArray([1,2,3,4,5,6,7,8,9])
print(array)
// add
array.append(0)
print(array)
array.append(contentsOf: [5,5,5])
print(array)
// filter
array = array.filter { $0 < 7 }
print(array)
// map
let strings = array.map { "\($0)" }
print(strings)
// insert
array.insert(99, at: 5)
print(array)
array.insert(contentsOf: [2, 2, 2], at: 0)
print(array)
// pop
_ = array.popLast()
print(array)
_ = array.popFirst()
print(array)
// remove
array.removeFirst()
print(array)
array.removeFirst(3)
print(array)
array.remove(at: 2)
print(array)
array.removeLast()
print(array)
array.removeLast(5)
print(array)
array.removeAll { $0%2 == 0 }
print(array)
array = AtomicArray([1,2,3,4,5,6,7,8,9,0])
array.removeSubrange(0...2)
print(array)
array.replaceSubrange(0...2, with: [0,0,0])
print(array)
array.removeAll()
print(array)
array.set(array: [1,2,3,4,5,6,7,8,9,0])
print(array)
// subscript
print(array[0])
array[0] = 100
print(array)
print(array[1...4])
// operator functions
array = [1,2,3] + AtomicArray([4,5,6])
print(array)
array = AtomicArray([4,5,6]) + [1,2,3]
print(array)
array = AtomicArray([1,2,3]) + AtomicArray([4,5,6])
print(array)
Пример использования 2
import Foundation
var arr = AtomicArray([0,1,2,3,4,5])
for i in 0...1000 {
// Single actions
DispatchQueue.global(qos: .background).async {
usleep(useconds_t(Int.random(in: 100...10000)))
let num = i*i
arr.append(num)
print("arr.append(\(num)), background queue")
}
DispatchQueue.global(qos: .default).async {
usleep(useconds_t(Int.random(in: 100...10000)))
arr.append(arr.count)
print("arr.append(\(arr.count)), default queue")
}
// multy actions
DispatchQueue.global(qos: .utility).async {
arr.set { array -> [Int] in
var newArray = array
newArray.sort()
print("sort(), .utility queue")
return newArray
}
}
}
Начиная с Swift 5.5 вы можете выразить это с помощью актера:
actor SyncArray<T> {
private var buffer: [T]
init<S: Sequence>(_ elements: S) where S.Element == T {
buffer = Array(elements)
}
var count: Int {
buffer.count
}
func append(_ element: T) {
buffer.append(element)
}
func remove(at index: Int) -> T {
buffer.remove(at: index)
}
}
Это не только делает код более простым и менее подверженным ошибкам, но и делает более явным потенциальное состояние гонки, указанное в другом ответе:
Task {
let array = SyncArray([1])
if await array.count == 1 {
_ = await array.remove(at: 0)
}
}
Здесь есть две точки подвеса, а это значит, что к моменту
.remove(at:)
называется, массив
count
могло измениться.
Такая операция чтения-записи должна быть атомарной, чтобы быть согласованной, поэтому вместо этого она должна быть определена как метод для актора:
extension SyncArray {
func foo() {
if buffer.count == 1 {
_ = buffer.remove(at: 0)
}
}
}
Здесь отсутствие точек подвеса говорит о том, что операция выполняется атомарно.
Небольшая деталь: в Swift 3 (по крайней мере, в XCode 8 Beta 6) синтаксис для очередей значительно изменился. Важные изменения в ответе @Kirsteins:
private let accessQueue = DispatchQueue(label: "SynchronizedArrayAccess")
txAccessQueue.async() {
// Your async code goes here...
}
txAccessQueue.sync() {
// Your sync code goes here...
}
Swift-Nio и Vapor Swift
Для тех из вас, кто использует Swift-Nio (или Vapor Swift, основанный на Swift-Nio), есть встроенное решение этой проблемы:
class MyClass {
let lock = Lock()
var myArray: Array<Int> = []
func networkRequestWhatEver() {
lock.withLock {
array.append(someValue)
}
}
}
Обратите внимание, что вы должны использовать тот же Lock
объект при изменении того же Array
объект (или Dictionary
, так далее.).
Вот ответ для Swift 4,
let queue = DispatchQueue(label: "com.readerWriter", qos: .background, attributes: .concurrent)
var safeArray: [String] = []
subscript(index: Int) -> String {
get {
queue.sync {
return safeArray[index]
}
}
set(newValue) {
queue.async(flags: .barrier) { [weak self] in
self?.safeArray[index] = newValue
}
}
}
Я думаю, что dispatch_barriers стоит изучить. Использование gcd для синхронизации является для меня более интуитивным, чем использование ключевого слова синхронизации, чтобы избежать изменения состояния из нескольких потоков.
https://mikeash.com/pyblog/friday-qa-2011-10-14-whats-new-in-gcd.html
Здесь есть отличный ответ, который ориентирован на многопоточность и не блокирует одновременное чтение: /questions/16565854/izbegajte-etogo-svisayuschego-ukazatelya-s-arc/16565864#16565864
Это написано в Objective C, но портирование на Swift тривиально.
@property (nonatomic, readwrite, strong) dispatch_queue_t thingQueue;
@property (nonatomic, strong) NSObject *thing;
- (id)init {
...
_thingQueue = dispatch_queue_create("...", DISPATCH_QUEUE_CONCURRENT);
...
}
- (NSObject *)thing {
__block NSObject *thing;
dispatch_sync(self.thingQueue, ^{
thing = _thing;
});
return thing;
}
- (void)setThing:(NSObject *)thing {
dispatch_barrier_async(self.thingQueue, ^{
_thing = thing;
});
}
Кредит Rob Napier
Подход:
использование DispatchQueue
синхронизировать
См:
http://basememara.com/creating-thread-safe-arrays-in-swift/
Код:
Ниже представлена грубая реализация потокаобезопасного массива, вы можете точно настроить его.
public class ThreadSafeArray<Element> {
private var elements : [Element]
private let syncQueue = DispatchQueue(label: "Sync Queue",
qos: .default,
attributes: .concurrent,
autoreleaseFrequency: .inherit,
target: nil)
public init() {
elements = []
}
public init(_ newElements: [Element]) {
elements = newElements
}
//MARK: Non-mutating
public var first : Element? {
return syncQueue.sync {
elements.first
}
}
public var last : Element? {
return syncQueue.sync {
elements.last
}
}
public var count : Int {
return syncQueue.sync {
elements.count
}
}
public subscript(index: Int) -> Element {
get {
return syncQueue.sync {
elements[index]
}
}
set {
syncQueue.sync(flags: .barrier) {
elements[index] = newValue
}
}
}
public func reversed() -> [Element] {
return syncQueue.sync {
elements.reversed()
}
}
public func flatMap<T>(_ transform: (Element) throws -> T?) rethrows -> [T] {
return try syncQueue.sync {
try elements.flatMap(transform)
}
}
public func filter(_ isIncluded: (Element) -> Bool) -> [Element] {
return syncQueue.sync {
elements.filter(isIncluded)
}
}
//MARK: Mutating
public func append(_ element: Element) {
syncQueue.sync(flags: .barrier) {
elements.append(element)
}
}
public func append<S>(contentsOf newElements: S) where Element == S.Element, S : Sequence {
syncQueue.sync(flags: .barrier) {
elements.append(contentsOf: newElements)
}
}
public func remove(at index: Int) -> Element? {
var element : Element?
syncQueue.sync(flags: .barrier) {
if elements.startIndex ..< elements.endIndex ~= index {
element = elements.remove(at: index)
}
else {
element = nil
}
}
return element
}
}
extension ThreadSafeArray where Element : Equatable {
public func index(of element: Element) -> Int? {
return syncQueue.sync {
elements.index(of: element)
}
}
}
Во -первых, objc_sync_enter не работает
objc_sync_enter(array)
defer {
objc_sync_exit(array)
}
причина, по которой objc_sync_enter / objc_sync_exit не работает с DISPATCH_QUEUE_PRIORITY_LOW
objc_sync_enter является примитивом крайне низкого уровня и не предназначен для непосредственного использования. Это деталь реализации старой системы @synchronized в ObjC.
для swift следует использовать вот так, как сказал @Kirsteins, и я предлагаю синхронизацию вместо async:
private let syncQueue = DispatchQueue(label:"com.test.LockQueue")
func test(){
self.syncQueue.sync{
// thread safe code here
}
}
Если вы хотите потокобезопасное взаимодействие с вашим массивом, вы должны синхронизировать свой доступ. Предлагается много альтернатив (и некоторые из них были опущены), поэтому давайте рассмотрим различные альтернативы синхронизации:
Последовательная очередь отправки: это простой и интуитивно понятный шаблон GCD.
Шаблон чтения-записи с параллельной очередью: это элегантное усовершенствование шаблона последовательной очереди диспетчеризации, использующее параллельную очередь с асинхронной «записью» (поэтому вызывающая сторона не ждет завершения записи) с барьером (для предотвращения любого одновременного взаимодействия). с «записью»), но предлагает одновременное «чтение» (позволяя увеличить параллелизм во время «чтения»). Это сложный и привлекательный шаблон, но на практике он полезен только в том случае, если преимущества параллельного «чтения» и асинхронной «записи» перевешивают накладные расходы GCD.
Замки:
это быстрый и простой механизм блокировки, который более эффективен, чем любая из альтернатив GCD для большинства сценариев:
extension NSLocking { func synchronized<T>(_ block: () throws -> T) rethrows -> T { lock() defer { unlock() } return try block() } }
это еще один механизм блокировки, который даже быстрее, чем , но его немного сложнее использовать из Swift. См . /questions/54198727/osspinlock-ustarel-v-ios-100-vmesto-etogo-ispolzujte-osunfairlock-iz-and-ltosloc/60203152#60203152 . Но в тех редких случаях, когда производительность имеет первостепенное значение, несправедливые блокировки являются убедительным решением.
Objective-C и
API: это не представляет практического интереса в мире Swift. Семафоры: концептуально он похож на подходы, основанные на блокировках, но, как правило, медленнее, чем любой из подходов, основанных на блокировках, и его можно не учитывать в данном разговоре.
Актеры: механизм синхронизации, предоставляемый системой параллелизма Swift 5.5. См. Защита изменяемого состояния с помощью акторов Swift .
Короче говоря, если использовать
На практике выбор механизма синхронизации не имеет значения в большинстве случаев использования. (И если вы выполняете так много синхронизаций, что разница в производительности становится существенной, вы можете подумать о том, как уменьшить количество точек синхронизации, прежде чем остановиться на конкретном механизме.) При этом старые механизмы синхронизации (семафоры,
Обозначив возможные механизмы синхронизации, следующий вопрос заключается в том, на каком уровне выполняется синхронизация. В частности, не раз предлагались обёртки свойств для всего массива. Это неизменно неподходящее место для синхронизации. Подход с обёрткой свойств обеспечивает атомарный доступ к массиву (что не совсем то же самое, что безопасность потоков), но обычно требуется более высокий уровень абстракции. Например, если один поток добавляет элементы, а другой читает или удаляет их, часто требуется синхронизация каждой из этих высокоуровневых задач, а не только отдельных обращений к массиву.
Чтобы улучшить принятый ответ, я бы предложил использовать defer:
objc_sync_enter(array)
defer {
objc_sync_exit(array)
}
// manipulate the array
и второй
func sync(lock: NSObject, closure: () -> Void) {
objc_sync_enter(lock)
defer {
objc_sync_exit(lock)
}
closure()
}
Основная и наиболее распространенная идея сделать что-то (например, коллекцию) потокобезопасным в Swift:
- Пользовательская (локальная) параллельная очередь
- Синхронное чтение . Чтение критического раздела (общий ресурс) через
sync
- Асинхронная запись с барьером