以前、以下の呟きをしましたが、わりと古い方法を利用しているコードが散見されるので、Swift 3での同期処理(排他制御)についてまとめてみます。
getterはsyncでSwiftぽいロックになってて良いけど( ´・‿・`)https://t.co/X2OrjyBmH7 で、Foundation.Lockを使わずにDispatchQueue.sync使おうという話があった。 https://t.co/77zzkNUGLj
— mono( ´・‿・`)🐶🍎📱⌚️ (@_mono) August 7, 2016
参考: Concurrent Programming With GCD in Swift 3 - WWDC 2016 - Videos - Apple Developer
かつての方法
WWDCスライドのpp.120-123に載っているのでご覧ください。
WWDCで勧められていたDispatchQueue.sync
を使った方法
基本的には、こちらが良いようです。実際、扱いも簡単です。
class Account {
private var _balance: Int = 0
// デフォルトは、シリアルキュー
private let lockQueue = DispatchQueue(label: "Account lock serial queue")
// attributesに.concurrentを指定すると並行処理されるキューとなり同期処理の用途には不適切
// private let queue = DispatchQueue(label: "", attributes: .concurrent)
var balance: Int {
// getterを同期処理に
// クロージャー内で返した型をgetterの戻り値として返せる
get {
return lockQueue.sync { _balance }
}
// setterを同期処理に
set {
lockQueue.sync { _balance = newValue }
}
}
// _balanceの増減操作をするには読み取りと書き込みをセットで括る必要あり
func add(_ value: Int) {
lockQueue.sync { _balance += value }
}
}
ポイントは以下です。
- シリアルキュー(
lockQueue
フィールド)を作成しそれを同期処理に用いる -
lockQueue.sync
で処理を括るだけで同期処理に出来る
また、sync
メソッドは、いくつかオーバーロードがあり、戻り値を返すことも出来て、上の例ではbalanceのget
のみ2つ目の戻り値ありの方、それ以外は1つ目の戻り値無しの方が使われています。
public func sync(execute block: () -> Swift.Void)
public func sync<T>(execute work: () throws -> T) rethrows -> T rethrows -> T
簡単にテストしてみると、無事に排他制御されていることが確認出来ます。
(下の例では、balance
のget
・set
は不使用)
let account = Account()
for i in 0..<1_000 {
DispatchQueue.global().async {
account.add(10)
// 次のように同期処理を介さず操作すると最終結果がズレる(低くなる)
//account.balance += 10
}
}
Thread.sleep(forTimeInterval: 1)
print("final balance: \(account.balance)") // 10 x 1_000 = 10_000
ただし、パフォーマンスは良くない
パフォーマンスはローレベルのロック手法に比べて悪いようですが、安全・簡単なので、そのあたりがどうしても気になる処理以外は、Apple推奨のDispatchQueue.sync
を使った方が良いかなと思っています。
あるいは、DispatchQueue.sync
では要件満たせない時も、ローレベルの別のやり方を使う必要になってくると思います。