有併發的地方就存線上程安全問題,尤其是對於 Swift 這種還沒有內建併發支援的語言來說執行緒安全問題更為突出。下面我們通過常見的陣列操作來分析其中存在的執行緒問題,以及如何實現一個執行緒安全陣列。
問題所在
因為無法確定執行順序,所以併發導致的問題一般都很難模擬和測試。不過我們可以通過下面這段程式碼來模擬一個併發情形下導致的資料競爭問題。
var array = [Int]()
DispatchQueue.concurrentPerform(iterations: 1000) { index in
let last = array.last ?? 0
array.append(last + 1)
}複製程式碼
這段程式碼中我們對陣列 array 進行了 1000 次併發修改操作,雖然有些誇張但是它能很好的揭示一些併發環境下陣列寫操作存在的一些問題。因為對於值型別來說 Swift 採用的是 Copy On Write 機制,所以在進行 Copy On Write 處理是可能陣列已經被另一個寫操作給修改了。這就造成了陣列中元素和資料的丟失現象,如下:
Unsafe loop count: 988.
Unsafe loop count: 991.
Unsafe loop count: 986.
Unsafe loop count: 995.複製程式碼
序列佇列
這應該是大家都能想到的一種最常見處理方式。 由於序列佇列每次都只能執行一個程式,所以即使有多個陣列寫操作程式我們也能確保資源的互斥訪問。這樣陣列是從設計的併發程式安全的。
let queue = DispatchQueue(label: "SafeArrayQueue")
queue.async() {
// 寫操作
}
queue.sync() {
// 讀操作
}複製程式碼
由於寫操作並不需要返回操作結果,所有這裡可以使用非同步的方式進行。而對於讀操作來說則必須採用同步的方式實時返回操作結果。但是序列佇列有一個最為明顯的缺陷:多個讀操作之間也是互斥的。很顯然這種方式太過粗暴存在明顯的效能問題,畢竟讀操作的頻率直覺上是要高過寫操作的。
併發佇列
採用併發佇列我們就可以很好的解決上面提到的多個讀操作的效能問題,不過隨之而來的就是寫操作的資料競爭。這與我們在學習作業系統是的 讀者-作者 問題本質上是一類問題,我們可以通過共享互斥鎖來解決寫操作的資料競爭問題。對於 iOS 來說它就是 GCD 中的寫欄柵 barrier 機制。
let queue = DispatchQueue(label: "SafeArrayQueue", attributes: .concurrent)
queue.async(flags: .barrier) {
// 寫操作
}
queue.sync() {
// 讀操作
}複製程式碼
上面程式碼中我們對非同步的寫操作設定了 barrier 標示,這意味著在執行非同步操作程式碼的時候佇列不能執行其他程式碼。而對於同步的讀操作來說,由於是併發佇列同時讀取資料並不會存在任何效能問題。
實踐
/// A thread-safe array.
public class SafeArray<Element> {
fileprivate let queue = DispatchQueue(label: "Com.BigNerdCoding.SafeArray", attributes: .concurrent)
fileprivate var array = [Element]()
}
// MARK: - Properties
public extension SafeArray {
var first: Element? {
var result: Element?
queue.sync { result = self.array.first }
return result
}
var last: Element? {
var result: Element?
queue.sync { result = self.array.last }
return result
}
var count: Int {
var result = 0
queue.sync { result = self.array.count }
return result
}
var isEmpty: Bool {
var result = false
queue.sync { result = self.array.isEmpty }
return result
}
var description: String {
var result = ""
queue.sync { result = self.array.description }
return result
}
}
// MARK: - 讀操作
public extension SafeArray {
func first(where predicate: (Element) -> Bool) -> Element? {
var result: Element?
queue.sync { result = self.array.first(where: predicate) }
return result
}
func filter(_ isIncluded: (Element) -> Bool) -> [Element] {
var result = [Element]()
queue.sync { result = self.array.filter(isIncluded) }
return result
}
func index(where predicate: (Element) -> Bool) -> Int? {
var result: Int?
queue.sync { result = self.array.index(where: predicate) }
return result
}
func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] {
var result = [Element]()
queue.sync { result = self.array.sorted(by: areInIncreasingOrder) }
return result
}
func flatMap<ElementOfResult>(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] {
var result = [ElementOfResult]()
queue.sync { result = self.array.flatMap(transform) }
return result
}
func forEach(_ body: (Element) -> Void) {
queue.sync { self.array.forEach(body) }
}
func contains(where predicate: (Element) -> Bool) -> Bool {
var result = false
queue.sync { result = self.array.contains(where: predicate) }
return result
}
}
// MARK: - 寫操作
public extension SafeArray {
func append( _ element: Element) {
queue.async(flags: .barrier) {
self.array.append(element)
}
}
func append( _ elements: [Element]) {
queue.async(flags: .barrier) {
self.array += elements
}
}
func insert( _ element: Element, at index: Int) {
queue.async(flags: .barrier) {
self.array.insert(element, at: index)
}
}
func remove(at index: Int, completion: ((Element) -> Void)? = nil) {
queue.async(flags: .barrier) {
let element = self.array.remove(at: index)
DispatchQueue.main.async {
completion?(element)
}
}
}
func remove(where predicate: @escaping (Element) -> Bool, completion: ((Element) -> Void)? = nil) {
queue.async(flags: .barrier) {
guard let index = self.array.index(where: predicate) else { return }
let element = self.array.remove(at: index)
DispatchQueue.main.async {
completion?(element)
}
}
}
func removeAll(completion: (([Element]) -> Void)? = nil) {
queue.async(flags: .barrier) {
let elements = self.array
self.array.removeAll()
DispatchQueue.main.async {
completion?(elements)
}
}
}
}
public extension SafeArray {
subscript(index: Int) -> Element? {
get {
var result: Element?
queue.sync {
guard self.array.startIndex..<self.array.endIndex ~= index else { return }
result = self.array[index]
}
return result
}
set {
guard let newValue = newValue else { return }
queue.async(flags: .barrier) {
self.array[index] = newValue
}
}
}
}
// MARK: - Equatable
public extension SafeArray where Element: Equatable {
func contains(_ element: Element) -> Bool {
var result = false
queue.sync { result = self.array.contains(element) }
return result
}
}
// MARK: - 自定義操作符
public extension SynchronizedArray {
static func +=(left: inout SynchronizedArray, right: Element) {
left.append(right)
}
static func +=(left: inout SynchronizedArray, right: [Element]) {
left.append(right)
}
}複製程式碼
通過 filePrivate 屬性 array 和 queue , SafeArray 成功的實現了大多數陣列常用功能,更為關鍵的是該型別併發安全:所有的寫操作都通過 barrier 方式的非同步進行,而讀操作則與內建 Array 沒有什麼區別。
需要注意的是:我們使用同樣的方式可以實現併發安全的 Dictionary 類似:SynchronizedDictionary。
接下來,我們可以對傳統的非併發安全陣列和 SafeArray 進行以下比較:
import Foundation
import PlaygroundSupport
// Thread-unsafe array
do {
var array = [Int]()
var iterations = 1000
let start = Date().timeIntervalSince1970
DispatchQueue.concurrentPerform(iterations: iterations) { index in
let last = array.last ?? 0
array.append(last + 1)
DispatchQueue.global().sync {
iterations -= 1
// Final loop
guard iterations <= 0 else { return }
let message = String(format: "Unsafe loop took %.3f seconds, count: %d.",
Date().timeIntervalSince1970 - start,
array.count)
print(message)
}
}
}
// Thread-safe array
do {
var array = SafeArray<Int>()
var iterations = 1000
let start = Date().timeIntervalSince1970
DispatchQueue.concurrentPerform(iterations: iterations) { index in
let last = array.last ?? 0
array.append(last + 1)
DispatchQueue.global().sync {
iterations -= 1
// Final loop
guard iterations <= 0 else { return }
let message = String(format: "Safe loop took %.3f seconds, count: %d.",
Date().timeIntervalSince1970 - start,
array.count)
print(message)
}
}
}
PlaygroundPage.current.needsIndefiniteExecution = true複製程式碼
得到的輸出可能如下:
Unsafe loop took 1.031 seconds, count: 989.
Safe loop took 1.363 seconds, count: 1000.複製程式碼
雖然由於使用了 GCD 機制導致速度慢了 30% 左右並且使用了更多的記憶體,但是與之對應的是我們實現了一個併發安全的陣列型別。
原文地址