原文地址
有并發(fā)的地方就存在線程安全問題,尤其是對(duì)于 Swift 這種還沒有內(nèi)置并發(fā)支持的語言來說線程安全問題更為突出。下面我們通過常見的數(shù)組操作來分析其中存在的線程問題,以及如何實(shí)現(xiàn)一個(gè)線程安全數(shù)組。
問題所在
因?yàn)闊o法確定執(zhí)行順序,所以并發(fā)導(dǎo)致的問題一般都很難模擬和測(cè)試。不過我們可以通過下面這段代碼來模擬一個(gè)并發(fā)情形下導(dǎo)致的數(shù)據(jù)競(jìng)爭(zhēng)問題。
var array = [Int]()
DispatchQueue.concurrentPerform(iterations: 1000) { index in
let last = array.last ?? 0
array.append(last + 1)
}復(fù)制代碼
這段代碼中我們對(duì)數(shù)組 array 進(jìn)行了 1000 次并發(fā)修改操作,雖然有些夸張但是它能很好的揭示一些并發(fā)環(huán)境下數(shù)組寫操作存在的一些問題。因?yàn)閷?duì)于值類型來說 Swift 采用的是 Copy On Write 機(jī)制,所以在進(jìn)行 Copy On Write 處理是可能數(shù)組已經(jīng)被另一個(gè)寫操作給修改了。這就造成了數(shù)組中元素和數(shù)據(jù)的丟失現(xiàn)象,如下:
Unsafe loop count: 988.
Unsafe loop count: 991.
Unsafe loop count: 986.
Unsafe loop count: 995.復(fù)制代碼
串行隊(duì)列
這應(yīng)該是大家都能想到的一種最常見處理方式。 由于串行隊(duì)列每次都只能運(yùn)行一個(gè)進(jìn)程,所以即使有多個(gè)數(shù)組寫操作進(jìn)程我們也能確保資源的互斥訪問。這樣數(shù)組是從設(shè)計(jì)的并發(fā)進(jìn)程安全的。
let queue = DispatchQueue(label: "SafeArrayQueue")
queue.async() {
// 寫操作
}
queue.sync() {
// 讀操作
}復(fù)制代碼
由于寫操作并不需要返回操作結(jié)果,所有這里可以使用異步的方式進(jìn)行。而對(duì)于讀操作來說則必須采用同步的方式實(shí)時(shí)返回操作結(jié)果。但是串行隊(duì)列有一個(gè)最為明顯的缺陷:多個(gè)讀操作之間也是互斥的。很顯然這種方式太過粗暴存在明顯的性能問題,畢竟讀操作的頻率直覺上是要高過寫操作的。
并發(fā)隊(duì)列
采用并發(fā)隊(duì)列我們就可以很好的解決上面提到的多個(gè)讀操作的性能問題,不過隨之而來的就是寫操作的數(shù)據(jù)競(jìng)爭(zhēng)。這與我們?cè)趯W(xué)習(xí)操作系統(tǒng)是的 讀者-作者 問題本質(zhì)上是一類問題,我們可以通過共享互斥鎖來解決寫操作的數(shù)據(jù)競(jìng)爭(zhēng)問題。對(duì)于 iOS 來說它就是 GCD 中的寫欄柵 barrier 機(jī)制。
[圖片上傳中...(image-407a8e-1569391139400-0)]
<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;">Barrier</figcaption>
let queue = DispatchQueue(label: "SafeArrayQueue", attributes: .concurrent)
queue.async(flags: .barrier) {
// 寫操作
}
queue.sync() {
// 讀操作
}復(fù)制代碼
上面代碼中我們對(duì)異步的寫操作設(shè)置了 barrier 標(biāo)示,這意味著在執(zhí)行異步操作代碼的時(shí)候隊(duì)列不能執(zhí)行其他代碼。而對(duì)于同步的讀操作來說,由于是并發(fā)隊(duì)列同時(shí)讀取數(shù)據(jù)并不會(huì)存在任何性能問題。
實(shí)踐
/// 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)
}
}復(fù)制代碼
通過 filePrivate 屬性 array 和 queue , SafeArray 成功的實(shí)現(xiàn)了大多數(shù)數(shù)組常用功能,更為關(guān)鍵的是該類型并發(fā)安全:所有的寫操作都通過 barrier 方式的異步進(jìn)行,而讀操作則與內(nèi)置 Array 沒有什么區(qū)別。
需要注意的是:我們使用同樣的方式可以實(shí)現(xiàn)并發(fā)安全的 Dictionary 類似:SynchronizedDictionary。
接下來,我們可以對(duì)傳統(tǒng)的非并發(fā)安全數(shù)組和 SafeArray 進(jìn)行以下比較:
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復(fù)制代碼
得到的輸出可能如下:
Unsafe loop took 1.031 seconds, count: 989.
Safe loop took 1.363 seconds, count: 1000.復(fù)制代碼
雖然由于使用了 GCD 機(jī)制導(dǎo)致速度慢了 30% 左右并且使用了更多的內(nèi)存,但是與之對(duì)應(yīng)的是我們實(shí)現(xiàn)了一個(gè)并發(fā)安全的數(shù)組類型。