Swift 世代の排他制御

  • 71
    いいね
  • 4
    コメント

Update: 関連する記事のリンクを追加しました。

Swift 3 世代の排他制御
http://qiita.com/codelynx/items/56ce2f91cd3f4f409aeb

今回は Swift で排他制御が必要になった時の TIPS を紹介したいと思います。

Objective-C時代の古き良き排他制御
GCDを使った排他制御
NSLock と defer を使った排他制御

Objective-C時代の古き良き排他制御

Objective-C の時代に排他制御のコードを書いた人は @synchronized をよく使ったと思います。簡単な構文で手軽に排他制御できていたので重宝していたかと思います。

// Objective-C 時代の排他制御

- (NSData *)readDataRange:(NSRange)range
{
    @synchronized(self) {
        // 排他制御したいコード
    }
}

- (void)writeData:(NSData *)data range:(NSRange)range
{
    @synchronized(self) {
        // 排他制御したいコード
    }
}

GCDを使った排他制御

Swift でプログラミングを書き始めてスレッド間で排他制御が必要になると、@synchronized を懐かしく思いますが、ない袖は振れません。そこで、GCD の DISPATCH_QUEUE_SERIAL を使った排他制御がよく紹介されています。このオプションで作成したキューで実行されるコードは、一つづつ順次実行されるので、複数のスレッド間で排他制御を実現する事ができます。また、dispatch_async() ではなく dispatch_sync() を使えば、呼び出し側のスレッドはクロージャの実行が完了するまで待たされています。

// GCD を使った排他制御

let queue = dispatch_queue_create("com.app.name", DISPATCH_QUEUE_SERIAL)!

dispatch_sync(queue, {
        // 排他制御したいコード
})

dispatch_sync(queue) {
        // 排他制御したいコード
}

問題もあります。戻り値があるような関数やメソッドの場合は、GCD のクロージャー(ブロック)内から戻り値を戻す事が出来ないので、以下のような書き方はできません。

func readDataRange(range: NSRange) -> NSData? {
    dispatch_sync(queue) {
        let data = // ...
        return data // error
    }
    return nil
}

NSLock と defer を使った排他制御

そこで、Swift 2 から追加された。defer@synchronized 以前から存在していた NSLock を組み合わせると、意外といい感じで排他制御ができるようになったので紹介したいと思います。

NSLock@synchronized が登場する以前から存在していた古典的なセマフォです。排他制御を始めたい時点で lock() を呼び出し、排他制御を必要とする処理が終わったところで unlock() を呼び出します。

あるスレッドが lock() してる間に、別のスレッドが 同NSLocklock() しようとすると、そのスレッドはブロックされ待たされる事になります。その間に先のスレッドが unlock() を呼べば、待たされていたスレッドのうち一つだけが、ブロック状態を解除され、排他制御を必要とする処理を行う事ができます。

NSLocklock() できたスレッドは unlock() を必ず呼び出されなくてはなりません。if 文や switch 文で複雑になった分岐の先に return 文がある場合は特に要注意です。一つでも、unlock() を忘れて return するパターンがあると、開かずの扉ができてしまいます。Objective-C の @synchronized を懐かしむのは、スコープを抜けた時に unlock と同等の事をしてくれるので、比較的安心して使う事が出来たからです。

Swift 2.0 では defer 文が追加されて、同様の処理が書きやすくなりました。以下の例のように、NSLockself.lock.lock() する直後(直前でもいいのですが)に defer { self.lock.unlock() } と書いておけば、どこかで不意に return されたとしても、スコープを抜けた時に unlock() が呼ばれるので、開かずの扉をうっかり作ってしまうリスクを回避する事ができます。

class MyObject {
    let lock = NSLock()

    // ...

    func readDataRange(range: Range<Int>) -> NSData? {
        self.lock.lock()
        defer { self.lock.unlock() }  // unlock を保証

        let data = // 読み込み処理
        return data
    }

    func writeData(data: NSData, range: Range<Int>) {
        self.lock.lock()
        defer { self.lock.unlock() }  // unlock を保証

        // 書き込み処理
    }
}

もちろん処理が複雑になると、lock した状態で、さらに別のメソッドを呼び出して、そこで同じ NSLock を lock しようとするとデッドロックになったりと、油断は禁物です。

と、ここまで書いてから、そういえば GCD にも dispatch_semaphore_t があったなと思いつつもまぁいい事とします。

[環境表示]

Apple Swift version 2.2 (swiftlang-703.0.18.1 clang-703.0.29)
Xcode 7.3 (7D175)