Swift 3での同期処理(排他制御)の基本

  • 64
    いいね
  • 0
    コメント

以前、以下の呟きをしましたが、わりと古い方法を利用しているコードが散見されるので、Swift 3での同期処理(排他制御)についてまとめてみます。

参考: Concurrent Programming With GCD in Swift 3 - WWDC 2016 - Videos - Apple Developer

かつての方法

WWDCスライドのpp.120-123に載っているのでご覧ください。

Screen Shot 2016-11-16 at 15.24.46.png

Screen Shot 2016-11-16 at 15.26.31.png

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

簡単にテストしてみると、無事に排他制御されていることが確認出来ます。
(下の例では、balancegetsetは不使用)

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では要件満たせない時も、ローレベルの別のやり方を使う必要になってくると思います。