本日は The Swift Programming Language の話題はお休みをして、調べものをしているときに見つけて気になった Concurrency
による並行処理と セマフォ
についての技術ブログを眺めてみます。難しめなお話で自分自身がまだ理解できていないですけれど、読み進めながら理解を進めて行ってみますね。どうぞよろしくお願いします。
———————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #201
00:00 開始 00:11 今回の展望 00:38 Concurrency でセマフォを作ってみよう、という話 01:04 考え方が窺えるブログに好印象 02:45 Swift Concurrency に持つ印象 05:43 Swift Concurrency の応答を待つ方法は? 09:23 Swift Concurrency を学んでみる 10:46 Concurrency をセマフォで止めて良いのかという疑問も 11:18 DispatchSemaphore のおさらい 13:17 セマフォとミューテックス 17:09 ブログに沿ってセマフォを作ってみる 17:28 Concurrency で DispatchSemaphore を使ってみる 18:56 試しにセマフォを作っていくスタイル 19:18 セマフォを作るのに使えそうな材料 20:35 Actor では実現できないかもしれない 21:25 TaskGroup でも実現できなそう 22:25 AsyncStream は使えそう? 24:19 yield を活用できる可能性の検証 25:18 AsyncStream は並行安全ではない 27:43 CheckedContinuation が使えそう? 28:26 次回の展望とクロージング ————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #201
今日は、検討会を始めたいと思います。今回は、公式の「The Swift Programming Language」から少し外れて、興味深いブログを題材にしてお話を進めたいと思います。具体的には、Swiftの並行処理についてです。このブログの著者はバンソンさんという方で、並行処理の実装やその検討過程をとても丁寧に紹介しています。
まず、ブログの内容を簡単に説明します。バンソンさんは、どのような方法を使えば目的を実現できるかといった仮説を立て、その仮説に基づいて一つ一つ実際に検討・実装しています。また、その際の懸念点や問題点も細かく調査し、非常に詳しくブログにまとめています。このブログは少し長いですが、じっくり読めばSwiftのコンカレンシーについて多くの学びが得られると思います。
さて、Swiftのコンカレンシーについてですが、どのように使われているか気になりますよね。業務でよく使われることが多いですが、個人的にプログラムを作る際にも使える機会はあります。ただ、実際に使うシーンがどれほどあるかと問われると、必ずしも多くはないかもしれません。それでも、アイデアを練ってここはコンカレンシーが使えそうだと判断すると、試してみる価値はあります。
Swiftのコンカレンシーは、一般的な並行処理に対するイメージよりも広い範囲で使えます。例えば、NotificationCenter
のaddObserver
メソッドなどを使うと、それが非同期シーケンスとして返ってきて、ループ内でメッセージが受け取れるたびに処理を実行できます。並行処理といっても、自分の中のイメージとは少し異なるかもしれません。
並行処理の使い心地についても考えてみましょう。起動機能の処理と並行処理は少し違います。並行処理は複雑な計算を並列で実行し、その結果を取得するというイメージがありますが、必ずしもそうでなくとも幅広く使えるのがSwiftのコンカレンシーの魅力です。
今回のテーマであるセマフォ(Semaphore)についても考察しましょう。セマフォはタスクの制御に使われますが、他の待機機能—例えば、GCDのdispatch_sync
メソッドのようなものはSwiftでは簡単には使えません。
ビューコントローラーが例えばNSTableViewDelegate
を準拠させている場合、そのメソッドがメインアクターでの実行を必要とするときに問題が発生します。この問題を解決するために、例えばビューコントローラーをアクターとして定義し、その中でデータを管理する方法を取ることができます。ただし、アクター内部で状態を管理しているとき、それを迅速に参照する方法には工夫が必要です。
最後に、プロパティを安全に返すには非同期タスクを作成してから呼び出す必要があります。しかし、これは実装次第でエラーが出ないようにすることも可能です。勉強会ですので、このような問題も含めて一緒に考えていければと思います。 後に返したい処理があります。これが具体的にできるのかどうか疑問に感じていました。セマフォを使ったりして止めることも考えたのですが、表示がうまくいかず、何をしたのか忘れてしまいました。結局、ディスパッチでセマフォを使って止めたり、dispatch_sync
やDispatchQueue
の同期処理を行う方が良いのか分からずにいます。この辺りの処理が難しく、並行処理なのか非同期処理なのか悩んでいました。使い道が限られてくるので日常的に使うことはないのかなと思っていたのですが、最近ではそこまで問題ないとも感じています。特に、ペーシングシーケンスを止める際にどう処理すれば良いのか気になっています。
おそらく多くの人がセマフォを使っているのではないかとも感じますが、自分で使うとなると機能が偏っていて、覚えていない機能もたくさんありそうです。そのため、このブログを読んでコンカレンシー(並行処理)の知識を深めたいと思いましたが、セマフォを本当に使って良いのか疑問もあります。ブログを参考にしながら、その辺りも理解していきたいと思います。
セマフォはリソース管理のための有名な機能です。たとえば、DispatchSemaphore
を使ってリソースの管理を行います。セマフォの初期値を設定して、そのリソースが利用可能かどうかを示します。イベント処理のような使い方をすることもあります。
DispatchQueue
を使って非同期処理を行い、その中で例えばスリープする処理を挟むことができます。その際にセマフォで待機し、処理が完了したらセマフォにシグナルを送って、待ちが解除されるようにします。具体的には以下のコードのようになります。
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.global().async {
// 長い処理をシミュレート
sleep(3) // 3秒待つ
print("処理完了")
semaphore.signal() // シグナルを送る
}
print("スタート")
semaphore.wait() // セマフォで待機
print("完了")
このようにして非同期処理を行い、セマフォで待機しながらシグナルが送られるのを待つことができます。これにより、非同期処理が完了するまで待機することができます。
セマフォの使い方に関しては、NSLock
やMutex
の使用も一般的です。これらの機能を使い分けることで、さまざまな並行処理を適切に制御できるようになります。 ミューテックス(mutex)について話していますね。ミューテックスは複数のスレッドが同時に同じリソースにアクセスするのを防ぐために使われる同期プリミティブです。基本的な動作としては、ミューテックスが1スタートだということです。要するに、セマフォの初期値が1でスタートし、ウェイトとシグナルを組み合わせることでミューテックスのように動作させるという感じです。
具体的には、セマフォが1の場合、それはバイナリーセマフォであり、それが実質的にミューテックスとなります。バイナリーセマフォを使ってリソースをロックし、その間にアトミックなリソース(競合状態が起きないリソース)を処理して、アンロックするという流れです。そのため、ミューテックスとバイナリーセマフォは本質的に同じような動きをするのです。
また、セマフォについても触れていますね。セマフォ自体はリソースを管理するために便利なツールですが、WWDCのビデオでは「ディスパッチセマフォはコンカレンシーで使うべきではない」と述べられているようです。それはディスパッチセマフォがアンセーフプリミティブであるためです。アンセーフプリミティブは、安全でない動作を引き起こす可能性があるため、一般的には使うことが推奨されていません。
ディスパッチセマフォを使うのはあまり勧められませんが、これをタスクに変えることで動作させることは可能です。たとえば、```await Task.sleep``を用いると、一定時間スリープさせることができます。Swiftの新しいバージョンで利用できる機能を使うと特に良いでしょう。
次にアクターを使ったコンカレンシーの制御についても言及されています。アクターはリソースのロック状態を管理するのに適しており、プログラムの状態を正しく保つために良い手段だということです。アクターを用いることで、同期の問題を回避しやすくなるようです。
しかし、アクターには待機処理(await
)の問題があると言っています。たとえば、await
を使わないと待機できないし、使う必要がある場面では使わなければならないため、使用場面が制限されてきます。タスクグループも似たような使い方になるようです。
概して、ミューテックスやセマフォ、アクターといった同期プリミティブを適切に使うことでリソースの競合状態を避けることができ、プログラムの正確性や安全性を確保することができるということですね。これらを使いこなすことで、より堅牢なプログラムを作成できるようになります。 話の内容を整えますと、以下のようになります。
この方の感覚では、もっと詳しく調べなくても色々とイメージを膨らませている感じがすごいです。自分としては、もう少し詳しく調べたいとは思うけれども、結論が出ているようです。とりあえず、この方法は向いていないということがわかりました。
エージングストリームは、await
で順次取れるやつですよね。これでセマフォーのように待機するわけではないので、全然性格が違いそうな気がします。それでも気になるから、ブログの作者が今どう考えているかを見てみます。
エージングストリームは、エージングの使いやすさを向上させる機能です。それで移動機に順番にデータを取り出すストリームを作ることができ、値を取得するにはawait
を使う必要があります。await
を使うことは、セマフォーのwait
に近いかもしれませんが、違いもありますね。
確かにNSテーブルビューのデータソースの部分で、自分の場合はブロックしたいことがあります。しかし、await
を使われると待てないことがあり、タスクが少ないと問題になります。ですので、この方法は自分には向いていないかもしれません。
エージングストリームの使い方が書いてありました。「yield
」というメソッドがあり、このメソッドを使うとエージングストリームがデータの提供を開始し、それまでは待機するということですね。確かに待たせてその間に何かをするという雰囲気には近いですが、自分の理想とは反します。
これまで説明されていた「yield
」ですが、これを複数のタスクから呼んでも問題ないという点はとても良いです。平行して複数のタスクにコンティニュエーションで渡しても問題ないみたいです。
ただ、エージングストリームがセンダブルに準拠していないというのが問題です。エージングストリームのイテレーターはスレッドをまたげないので、次の値をawait
することができません。いろんなタスクにストリームを渡して使うときに問題が生じます。
実際に試してみたところ、フェイタレーラーが発生したようです。エージングストリームのイテレーターをawait
するとフェイタレーラーが起きたとのことです。つまり、意図的にブロックされるように設計されているようです。
このブログを書いている方の見解によると、セミコロンを使っているのも評価が高いです。色々と学ぶことが多いですが、まとめると、エージングストリームは今回の目的には向いていなかったようです。
次回は、セマフォーの内部状態を管理する方法として、アクターを用いることに焦点を当ててみることになりそうです。この方法について読んでいくことが、勉強にもなりそうです。
ということで、今日はこれで勉強会を終わりにしましょう。お疲れ様でした、ありがとうございました。