Swiftでセマフォ（semaphore）にさわってみよう

# はじめに

最近セマフォ（semaphore）について振り返る機会があり、Swiftでの動作を確認する意味もあってSwiftでセマフォの[サンプルソースコード](https://github.com/BlueEventHorizon/SemaphoreExample)を作成しました。

そのソースコードを用いて、「セマフォって聞いたことはあるけど？？」と思っているエンジニアのために本記事を書いてみました。

# セマフォ（semaphore）とは

下記は[wikipediaからの引用](https://ja.wikipedia.org/wiki/セマフォ)です

>セマフォ（英: semaphore）とは、計算機科学において、並列プログラミング環境での複数の実行単位（主にプロセス）が共有する資源にアクセスするのを制御する際の、単純だが便利な抽象化を提供する変数または抽象データ型である。

セマフォを理解するキーワードは、**「並列」**と**「資源」**だと思います。

**「並列」**な動作を行う複数のスレッドやプロセスが、共有する**「資源」**を正しく利用できる（アクセス制御する）仕組みがセマフォです。

まずは、セマフォがない場合に何が起こるか、の例をSwiftのソースコード [^ code1]で以下に提示します。

# セマフォ（semaphore）がない場合

[^ code1]: このソースコードは分かりやすくするために実行するのに必要ないくつかのコードが省かれています。実際に動作させてみたい方は、[こちら](https://github.com/BlueEventHorizon/SemaphoreExample)をご覧ください。

このソースコード内では、resourceが「資源」で、スレッド１、スレッド２が「並列」に動作します。
スレッドは何度も繰り返すタスクをもっていて、それを完了すればスレッドも終了します。
それぞれのタスクは、resourceがゼロ以上の場合は、自分のカウンターを１加算してresourceを１減算します。resourceがゼロになれば処理を終了します。

また`DispatchQueue.global(qos: .background).async`（ [リファレンス👉](https://developer.apple.com/documentation/dispatch/dispatchqueue/2300077-global)） がそれぞれ使われており、qosが同じレベルのため、ほぼ均等のタイミングで実行状態が切り替わります。

[^ preemptivemaultitask]: （CPUの）割り込み機構を使って現在実行中のプロセスを中断させ、スケジューラを呼び出して次に実行すべきプロセスを決定させる。それによってある一定時間内で、CPU時間は全てのプロセスに（配分はどうであれ）分配される。プリエンプティブ・マルチタスクでは、オペレーティングシステムのカーネルは優先順位に従ったスケジューリングがなされるようにコンテキストスイッチを自発的に行うこともでき、それが実行中タスクのプリエンプションとなる。高い優先順位のタスクが実行されるように現在実行中のタスクから権限を奪うのが、プリエンプティブ・スケジューリングである。（https://ja.wikipedia.org/wiki/プリエンプション）より抜粋

```swift

func testHandleResourceWithoutSemaphoreQiita() throws {

    var resource: Int = 100
    var task1_counter: Int = 0
    var task2_counter: Int = 0

    // resourceがゼロ以上の場合は、自分のカウンターを１加算してresourceを１減算する
    func task1() -> Bool {

        var value = resource

        guard value > 0 else {
            return false
        }

        task1_counter += 1
        value -= 1
        resource = value

        return true
    }

    // resourceがゼロ以上の場合は、自分のカウンターを１加算してresourceを１減算する
    func task2() -> Bool {

        var value = resource

        guard value > 0 else {
            return false
        }

        task2_counter += 1
        value -= 1
        resource = value

        return true
    }

    // スレッド 1
    DispatchQueue.global(qos: .background).async {
        var executing = true
        while executing {
            executing = task1()
        }
    }

    // スレッド 2
    DispatchQueue.global(qos: .background).async {
        var executing = true
        while executing {
            executing = task2()
        }
    }

    // ２つのスレッドが完了するまで待つ

    print("task1_counter = \(task1_counter)")
    print("task2_counter = \(task2_counter)")
}
```

結果は、

>task1_counter = 88
task2_counter = 102

だったり

>task1_counter = 100
task2_counter = 0

だったり

>task1_counter = 13
task2_counter = 101

します。

>resourceがゼロ以上の場合は、自分のカウンターを１加算してresourceを１減算します。resourceがゼロになれば処理を終了します。

であればtask1_counterとtask2_counterの合計は100になりそうなものですが、そうはなりません（なることもあります）

これはなぜかというと、

```swift

var value = resource

guard value > 0 else {
    break
}

task2_counter += 1
value -= 1
resource = value
```

この処理を行っている間に、別の処理が割り込んできて、resourceの値を書き換えてしまうからです。
つまり、変数resourceはアクセス制御されていません。`スレッドセーフではない`、という言い方がよくされます。

# セマフォ（semaphore）を使ってみる

関数の先頭で

```swift

let semaphore  = DispatchSemaphore(value: 1)
```

を宣言します。

`DispatchSemaphore(value: 1)` で、アクセス制御するリソースは１つだけだと宣言しています。リソースとは、

```swift

var resource: Int = 100
```

のことです。
この変数をアクセス制御できれば、おかしな現象は起こりません。

続いて、先ほどの割り込みされるtask1(), task2()の先頭でそれぞれ

```swift

defer {
    semaphore.signal()
}
semaphore.wait()
```

を追加します。
これで、関数に入った時点で `semaphore.wait()`が実行され、関数を抜けるタイミングで必ず `semaphore.signal()` が実行されます。
wait()ではDispatchSemaphoreで指定したカウンタを１減算し、signal()で１増加させます。カウンタがゼロの状態でwait()を呼び出したスレッドは、後述のようにBlocking状態となり処理は停止します。このスレッドは、signal()によってリソースが解放されるまで**OSによってブロックされ**ます。 [^ block]

[^ block]: https://ja.wikipedia.org/wiki/プロセス#ブロック状態 を参照

このような仕組みを導入することで、task1_counterとtask2_counterの合計は必ず100になることが保証されます。

### スレッド/プロセスの状態遷移

<img width="610" alt="スクリーンショット 2020-05-30 15.24.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37634/35ebcfcc-5a80-7f8f-2132-e200191e39fc.png">

Running（実行中）状態で、wait()を呼び出して運悪く誰かがリソースを使用していた場合は、Blocked状態になります。その後利用していたスレッドがsignal()を発行してリソースを解放すると、うまくOSスケジューラに拾ってもらえれば、Waiting状態に遷移し、無事Runningに復帰し、リソースにアクセスする権限を得られます。

上記の状態遷移図はiOSのものとは若干異なるのかもしれませんが、おおよそ同じような動きになるはずです。（正式な情報をお持ちでしたらお教えください）

### セマフォがない場合のコンテキストスイッチ

アクセス制御がなされていない最初のコードでは、下記のようにスレッドが強制的にWaiting状態にスイッチされる（プリエンプションされてコンテキストスイッチが発生する）ことでtaskが中断しています。この中断の間に別のスレッドで、変数resourceが操作されるのが問題です。


<img width="905" alt="スクリーンショット 2020-05-30 14.57.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37634/a4a70d31-52d7-0f5a-8a66-fa311400f358.png">

繰り返しになりますが、ここで重要なのはセマフォがアクセス制御しているのは、リソースである変数resourceです。当該のリソースが何なのかを意識しないで漠然とセマフォを利用すると痛い目に遭います。要注意です。

### セマフォがある場合のコンテキストスイッチ

セマフォを用いた状態遷移は下図のようになります。task1がセマフォを取得するとtask2はwait()を読んだ時点でBlocking状態に遷移します。のちにOSによって実行許可が出た場合は、Waitingを介してRunning状態に遷移し、wait()関数を抜けることになります。

<img width="900" alt="スクリーンショット 2020-05-30 16.47.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37634/acef835f-54fc-5440-7ce5-904b7c8f3ba4.png">


セマフォが解放されたタイミングで次にどちらのスレッドが割り当てられるかは、OSのスケジューリングに寄ります。これを制御しようと思うのならば、

```swift

DispatchQueue.global(qos: .background).async
```

のQoS（[リファレンス👉](https://developer.apple.com/documentation/dispatch/dispatchqos)）を変更することである程度は可能です。QoSとはquality of serviceの略で、優先制御と呼ばれることが多いです。例えばインターネットで動画を途切れなく見るためにはQoSが有効なネットワークを利用することが必要だったりします。この場合は優先制御の他にも帯域制御が行われたりします。（[参考記事👉](https://network.yamaha.com/knowledge/qos)）
ここでは、iOSのスレッドスケジューリングの優先制御に影響を与えるパラメータと言うことができるでしょう。

DispatchQueue.globalでは下記のようなオプションが用意されています。（[リファレンス👉](https://developer.apple.com/documentation/dispatch/dispatchqos/qosclass)）

| 定義値 | 説明 | 補足 |
|:----:|:---|:---|
| userInteractive |The quality-of-service class for user-interactive tasks, such as animations, event handling, or updating your app's user interface.|アニメーションやUIの更新などはモバイルアプリにとっては重要です。そこでiOSではuserInteractive には最も高い優先順位が与えられています[^ highqos]|
| userInitiated |The quality-of-service class for tasks that prevent the user from actively using your app.|ユーザが開始したアクション、またそれに付随する動作、結果に対して高い優先順位を与えるオプションです
|`default`|The default quality-of-service class.|標準優先順位|
| utility |The quality-of-service class for tasks that the user does not track actively.|すぐに実行する必要がない処理に対して指定する|
| background |The quality-of-service class for maintenance or cleanup tasks that you create.|バックグラウンドでCPUリソースなどに余裕がある時に実行される優先順位|
| unspecified |The absence of a quality-of-service class.|

（[👉参考記事](https://qiita.com/Saayaman/items/e603693a9bf967821e5d)）


[^ highqos]: モバイルアプリケーションではそうですが、例えば録画再生機器などは録画制御の優先順位が高く設定されており、ついで再生制御、UIの制御は比較的低く設定されていたりします。また、RTOSでは絶対的に他のスレッドより優先される、つまり絶対に譲らないリアルタイムスケジューリングスレッドも存在します。RTOSの概要は[こちら](https://www.tron.org/ja/onwebseminar/chap3/)

# カウンティングセマフォ(Counting Semaphore)


```swift

let semaphore  = DispatchSemaphore(value: 1)
```

ではリソースが１つのために初期値として`1`を与えています。

リソースが１つだけのセマフォを**バイナリーセマフォ（Binary Semaphore）**と呼びます。厳密には違う([参考時事👉](http://bkmts.xsrv.jp/mutex-semaphore/))のですが、**ミューテックス（Mutex）**と呼ばれることもあります。

リソースが２以上のセマフォはカウンティングセマフォ（計数セマフォ）と呼ばれ、アクセス制御に使うことはできません。
実は筆者も使ったことはありません😅

実際に2にしたらどうなるでしょうか。

[サンプルソースコード](https://github.com/BlueEventHorizon/SemaphoreExample)の中でも実験していますが、興味がある方は試してみてください。🏃‍♂️🏃‍♂️🏃‍♂️🏃‍♂️

# イベントフラグ（Event Flag）

イベントフラグにも触れておきたいと思います。
イベントフラグとは、主に組み込みプログラミングなどで利用されるμITRONで使われる用語です。実はPOSIXなどにもこの機能は明示されておらず、良い名称が無いのでイベントフラグという名称を使いたいと思います。

イベントフラグについてはこちらの記事[ITRON入門 イベントフラグで学ぶタスク間同期・通信機能](https://monozukuri-c.com/itron-eventflg/)を参照してください。

[サンプルソースコード](https://github.com/BlueEventHorizon/SemaphoreExample)に記載されているソースコードを紹介します。

このサンプルでは、`DispatchSemaphore(value: 0)`で初期値をゼロとしています。バイナリーセマフォがスレッドをBlockすることを利用して、別のスレッドにイベントフラグとしてをsignalを送信し、Blockを解除します。

```swift

    func testSingleEventFlag() throws {

        let semaphore  = DispatchSemaphore(value: 0)  // ⚠️ イベントフラグの初期化

        let expectation1 = XCTestExpectation(description: "expectation1")
        let expectation2 = XCTestExpectation(description: "expectation2")

        semLog.format("✳️ Start")

        DispatchQueue.global(qos: .background).async {

            usleep(1000_000)

            semaphore.signal() // ⚠️ イベントフラグを送信

            semLog.format("✳️ Sent EventFlag")

            expectation1.fulfill()
        }

        semaphore.wait() // ⚠️ イベントフラグを受信

        semLog.format("✴️ Recieved EventFlag")

        expectation2.fulfill()

        wait(for: [expectation1, expectation2], timeout: 10.0)
    }
```

こちらの実行結果は、下記のようになります（semLog.format()は自作のログ出力ツールです。[サンプルソースコード](https://github.com/BlueEventHorizon/SemaphoreExample)の中にあります）

テストを開始してから約１秒後にbackgroundのスレッドからイベントフラグの送信が起こり、待ち受けていたメインスレッドがBlockingからRunningに状態遷移して続きの処理が実行された様子が分かります。

```

    /*
     ✳️ Start [18:37:25.837] [main]
     ✳️ Sent EventFlag [18:37:26.847] [com.apple.root.background-qos]
     ✴️ Recieved EventFlag [18:37:26.847] [main]
     */
```

リソースのアクセス制御という観点から見ると、最初に説明した手法が`変数`などを守るためにあるのに対して、こちらはスレッドそれ自身を対象にしています。そのためスレッドが、いつ生成されて、いつ消滅するのかを考慮して設計、実装しなければなりません。

上記のようなシンプルなものであれば良いのですが、大規模なマルチスレッドのシステムに**動的に**適用しようとすると、`イベントフラグの初期化と送受信のタイミング`が問題になることもあります。

# デッドロック（Deadlock）/ リソーススタベーション（Resource Starvation）

最後に、バイナリセマフォ、ミューテックスで必ず取り上げられるデッドロックについても紹介いたします。

下記は、二つのスレッドが、互いの管理するリソースを参照して動作しようとしてデッドロックします。
大きな問題は、wait() → signal()の間に、外部の（しかも相互参照している）オブジェクトを呼び出していることです。
外部のオブジェクトが実は、自分のリソースを操作する関数を呼び出していると知らなければ、容易にデッドロックが発生します。

```swift

func testDeadlockBySemaphore() throws {

    var expectations: [XCTestExpectation] = [XCTestExpectation]()

    class SemThread {
        let semaphore = DispatchSemaphore(value: 1)
        var resource: Int = 0
        var other: SemThread?
        let expectation: XCTestExpectation
        let name: String

        // 生成時にセマフォを取得し、外部からのインクリメントをブロックする
        init(name: String) {
            self.name = name
            expectation = XCTestExpectation(description: name)
        }

        // otherに対してカウントアップ要求を出す。その後、外部からのインクリメントを許可
        func run() {
            DispatchQueue.global(qos: .background).async {

                self.increment()

                self.expectation.fulfill()
                semLog.format("⭐️\(self.name) completed")
            }
        }

        func increment() {

            semLog.format("⭐️\(self.name) will wait")
            semaphore.wait()

            usleep(100)

            semLog.format("⭐️\(self.name) make other increment")
            // 自セマフォを取得中に、相互参照している外部オブジェクトを利用する
            self.other?.increment()

            resource += 1
            semaphore.signal()
        }
    }

    let thread1 = SemThread(name: "thread1")
    let thread2 = SemThread(name: "thread2")

    expectations.append(thread1.expectation)
    expectations.append(thread2.expectation)

    // クロス参照する
    thread1.other = thread2
    thread2.other = thread1

    // スレッド開始
    thread1.run()
    thread2.run()

    // ２つのスレッドが完了するまで待つ（が、必ず失敗）
    wait(for: expectations, timeout: 5.0)
}
```

iOSではあまりセマフォを使う機会はないのですが、安易に導入すると、このようなデッドロックを引き起こす可能性があります。セマフォが本当に必要なのか、対象となるリソースは何なのか、相互参照はあるのか、wait() → signal()区間でreturnしていないか、などを十分に吟味して利用することが必要です。

iOSの場合、このようなプリミティブな操作を使うことなく、マルチスレッドを使いこなすために、DispatchQueueが用意されています。
セマフォの説明を延々した後でなんですが、まずDispatchQueueを使うことを検討した方が良いかもしれません。（自戒😇）

DispatchQueueとマルチスレッド操作については、別途記事を書いてみたいと思います。