本日も再び、前々回から読み進めている、調べものをしているときに見つけて気になった Concurrency
による並行処理と セマフォ
についての技術ブログの続きを見ていきます。前回の最後の方では本題から逸れて withCheckedContinuation
の特殊性みたいなところを見ていく感じの流れでしたけれど、今回はまた実装の話に戻っていよいよ wait
と signal
についてのコードについて触れられている後半になります。どうぞよろしくお願いしますね。
————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #203
00:00 開始 00:47 今回の展望 01:41 isolated 引数 04:39 同一 Actor へのアクセスが異なるスレッドで行われることも 04:52 isolated な引数の使い道は? 07:06 await の実装 08:25 待ち状態のタスク管理 09:32 別のタスクに再開を委ねる 12:14 wait が実装できたかの検証 15:09 同時呼出に耐え得る作りか判らない⋯ 17:35 signal の実装 19:45 CheckedContinuation は Sendable 20:28 これまでのセマフォ実装コード 21:50 考慮していない2つの課題 22:06 セマフォ解放時の待ちタスクの扱い 22:58 セマフォをキャンセルする想定 24:02 単一スレッドによる排他制御 24:40 セマフォでキャンセルを扱う際の注意 25:31 クロージングと次回の展望 —————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #203
はい、では始めていきましょう。今日も引き続き、Swiftのコンカレンシーについて勉強していきます。また、「手間を作る」というブログを参考にして進めていきますが、ちょっと思っていたものとは違う気もします。ただ、コンカレンシーの勉強としては良い題材ですので、最後まで読んでからどのような書き方ができるのか学んでいこうと思います。この後半を読み進めていきますね。
前回はアイソレイテッドアクターのところを読んでいました。パラメーターについても触れましたが、あまり使っていなかったので理解が不十分な気がしました。もう一度軽く見てみましょう。
アイソレイテッドアクターに関しては、具体的な例があったような気がしますが、今はこれで進めます。アイソレイテッドアクターについて考えると、どういった場面で使うのかが曖昧だったので、もう少し掘り下げてみようと思います。
例えば、関数が func action
としてあり、パラメーターを受け取ります。そして、アクターとして MyActor
を用意します。これで例えば、パラメーターの型を Int
にして用意します。以下のように書けます。
func action(parameter: Int, actor: isolated MyActor) {
// このアクションというメソッドが、アクションという関数になる。
await actor.someMethod()
}
このようにすると、このメソッドはアクターのプロパティやメソッドに対して隔離された状態でアクセスできるようになります。例えば、以下のように MyActor
のプロパティ x
に値を代入します。
actor.x = parameter
このコードで、MyActor
のプロパティ x
にパラメーターとして渡された値が設定されます。ここでは、await
を使用して非同期関数として動作させています。このようにアクターを利用すると、プロパティが無理なく安全に操作できる状態が保証されます。
さらに具体例として、setX
メソッドを作成することもできます。
func setX(value: Int) {
self.x = value
}
これを実際のシナリオで使うとき、await actor.setX(value)
のようにして呼び出します。これによって、アクターのプロパティ x
に安全に値を設定することができます。この方法はスウィフトらしい書き方ではなく、特別なケースで使われることが多いですが、便利です。
このように、アイソレイテッドアクターを使うことで、スレッドセーフな非同期処理を実現することができます。理解が深まってきたので、次に進みましょう。具体的な場面での使い方を考えながら、さらに学んでいきたいと思います。 コンカレンシーを理解する人にとっては、アウェイトやアイソレーティングといったキーワードは重要なものです。さて、前回の続きとして、今度は具体的にアウェイトを作成していく話に進みます。
まず、今まで書いてきたコードです。このコードでは、ウェイト
関数で変数バリュー
の値を1減らし、その結果を確認します。これはアクターのコンテキストで実行されています。仮にバリュー
がゼロ以上であれば、リソースに余裕があるので待つ必要はありません。ただし、この方法でリソース管理を行うためにセマフォを使用した経験は少ないのですが、多くの場合バイナリーセマフォを使ってリソースをロックすることが多いです。この場合も変数バリュー
の値は0か1です。
今、作成しているアウェイト
関数では、バリュー
がゼロ以上なら待たずに済むようになっています。このコードでリステップコンティニュエーション
を使って、アウェイター(待機者)のコンテキストを保存しています。これにより、他の箇所からシグナルが呼び出されたときに、アウェイターがリソースを要求するコンテキストを再開できます。
このコードでは、ウェイターズ
にコンティニュエーションを追加しています。もしバリュー
がゼロ未満であれば、リステップコンティニュエーション
を利用してアウェイトを実行し、コンティニュエーションを受け取ってウェイターズ
に追加します。この処理により、待機状態に移行します。
さらにこのコードで特徴的なのは、コンティニュエーションがセンダブルである点です。タスクがコンティニュエーションを保持し、そのタスク内でコンティニュエーションを実行することができます。このコードをXcodeのビルドオプションで最大限に守るように設定することで、警告が出ないことを検証しています。
一部の環境では特定の警告が出ることがあります。例えば、チェックドコンティニュエーションがノンセンダブルとして認識されるバグの報告がありましたが、この問題は特定のバージョンに影響するもので、過度に心配する必要はないでしょう。とにかく、ウェイトが呼び出されるときに、バリュー
がゼロでウェイターズ
が空であることを確認し、正しい順序で実行できるかを検証しています。
このような詳細な記述は、単なるリファレンス以上に実践的で有用な情報を提供してくれます。特にアクターの実行環境に関連するアイソレイティブの話は興味深いですね。最終的には、アクターのコンテキストでバリュー
を1減らす操作が実行されることになります。 コードが見えていると理解しやすいですね。画面の解像度によっては並べられない場合がありますが、できる限り工夫して並べてみましょう。たとえば、コードをWebスポーツの隣に配置すると良いですね。
例えば次のような例が考えられます。
// Actorの定義
actor MyActor {
func performTask() async {
// 処理内容
}
}
このコードのウィンドウをもう一つ作成して、最大化してみましょう。同じウィンドウ内にもう一つのコードウィンドウを追加したり、ウィンドウを並べて最大化したりします。これにより、複数のウィンドウを操作してコードを比較することができます。
次に、アクターメソッドの実行についてお話しします。With Checked Continuation
はアクターには含まれませんが、実行環境として継続されます。このため、アクター内で動作します。このあたりの理論は少し複雑に感じますが、Continuationが生成されてアクターの実行環境のまま動作するということになります。
たとえば、次のようなコードがあるとします。
await withCheckedContinuation { continuation in
continuation.resume(returning: value)
}
このコードでは、await
中に他の操作を行うため、制御フローが別のタスクに移る可能性があります。従って、このawait
部分が呼ばれた時点で、バリューが減少することになります。ただし、同じアイソレーション内にいるので基本的には安全です。
続いてシグナルについて説明します。シグナルの実装はシンプルです。待っているContinuationをリジューム(再開)させます。具体的には、次のような処理が考えられます。
if let waiter = waiters.first {
waiters.removeFirst()
waiter.resume()
}
このコードでは、待っているContinuationがあればそれを取り出してリジュームさせるという動作を行います。リジュームはそのまま継続され、待っていたタスクが再開されることになります。
バリューのチェックに関しても、次のようなアサーションを行うことで安全性を確保します。
assert(waiters.isEmpty)
アサーションがリジュームの後になると、実行に不正な状態を許すことになるので、注意が必要です。
以上のような構成で実装を行うと、より理解が進むかと思います。細かい部分はまた調査が必要な場面もありますが、基本的な流れはこのようになっています。 エッグコンティニュエーションの定義について説明します。エッグコンティニュエーションはセンダブルなもので、ファクターのような話ではありません。これを作る際に重要なのは、ウェイトとシグナルをリソース数で管理することです。この方法により、セマフォを作り、ウェイターズとして待ち行列を作成し、バリューとしてリソース数を管理します。イニシャライザーでリソース数を取る仕様です。
実際のセマフォのように機能するインターフェースが用意されており、基本的にはアサートを使って矛盾を確認し、矛盾があればプログラムを落とします。このアサートは矛盾がないことを確認するためだけに入れていますので、基本的にはエラーが出ないように設計されています。
しかし、まだ完全に試してはいませんが、大丈夫であることを確認するための措置としてアサートを置いています。ウェイトの場合はリソースを減らして待ち行列に処理を追加し、シグナルではリソースを増やして待ち行列を1つ解除する仕組みです。これにより、リズムをかけて行列を処理していくことができます。
このコードは理解を深めるためには良いですが、プロダクションで使用するには少し不安が残る状態です。具体的には、セマフォが開放されるときにウェイトアウトが残っている場合の対処や、ディスパッチセマフォの開放時の調整終了、タスクをどうキャンセルするべきか、セマフォのアウェイトをどうキャンセルするかが未解決です。例えば、デイニシャライザーでキャンセルすることも考えられますが、デイニシャライザーは現在アイソレーテッド環境外で動作しますので、将来的な対応が必要です。
タスクのスリープに似た方法でタイムアウトを設定することや、シグナルに数値を渡す仕様も検討中です。セマフォをリソース管理で使用した経験が少ないため、特定のスレッドに統一するロックフリーの考え方が適しているかもしれません。ロックフリーの概念は難しいですが、待たせない設計が目標となります。
キャンセルに関しては、ウェイト呼び出し中のキャンセルを意識することが重要です。キャンセルのタイミングでウェイトが終了するとリソース管理が崩れる可能性があります。したがって、ウェイトをスローズにするのが良いかもしれません。ディスパッチセマフォのタイムアウト発動時には、カウントが延長されないまま次に進む可能性があるため、スローズによる強制的な対処が良い選択かもしれません。
以上で説明は終わります。次回また詳細について話していきましょう。今日はここまでにします。ご清聴ありがとうございました。