非同期処理を含む終了処理
特にアンマネージリソースを扱う場合において、終了処理(キャンセルを含みます)では非同期処理を行うことがよくあります。
-
IAsyncDisposable
インターフェイスを実装するクラス -
System.Threading.Channels
クラスのCompleteAsync()
メソッドのような、適切なタイミングで明示的に実行する終了処理 - アンマネージリソースを使って終了時のログを書き込む場合に1回だけ行う
WriteAsync
的な普通の非同期メソッドの実行 - RDB処理が終わったときに行うトランザクションの後片付け
最近のAPIにはAsync
メソッドが標準搭載されているものも多いため、終了イベントに行うメソッドをasync
にしてawait
したくなります。しかし、一部の終了処理ではawait
すると処理の完了を待たずに終了してしまうこともあり、ハマることがあります。
本稿ではawait
してはいけない処理を2つ紹介します。
WinForms
のFormClosing
イベントおよびFormClosed
イベント
WinForms
には画面を閉じるときのイベントとして以下があります。
-
FormClosing
イベント- 画面を閉じる直前に発動します。
- キャンセルして閉じないことも可能です。
-
FormClosing
イベントを抜けるまでに閉じるのをキャンセルされていなければ画面を閉じてFormClosed
イベントに進みます。
-
FormClosed
イベント- 画面が閉じたあとに発動します。
- もう閉じているのでキャンセルはできません。
- スタートアップオブジェクトの場合、
FormClosed
イベントを抜けるとアプリケーションが終了します1。
イベントハンドラは当然ですがasync
にできます。async
にして内部でawait
した場合、メソッドを一回抜けて非同期で処理を待ちます。
・・・おわかりでしょうか?FormClosing
イベントもFormClosed
イベントも、メソッドを抜けた段階で次のイベントに進んでしまいます。そのため、この2つのイベントハンドラでawait
するとその行以降の処理が行われずにそのまま画面が閉じます。スタートアップオブジェクトでこれをやると、await
しているTask
が未完了でもアプリケーションが即終了します。
このため、FormClosing
およびFormClosed
では非同期処理をawait
してはいけません。
コンソールアプリのCancelKeyPress
イベント
コンソールアプリはCtrl+C
キーでアプリケーションを終了させることが可能です。このCtrl+C
キーが押されたときになにか処理をしたい場合、Console.CancelKeyPress
イベントにイベントハンドラを登録できます。このイベントハンドラで終了をキャンセルすることもできます。なお、このイベントが抜けるとコンソールアプリは終了します。
・・・もうお気づきですね?WinForms
の場合と同様に、CancelKeyPress
イベントのハンドラでawait
すると完了を待たずにアプリケーションが即終了します。
まとめ
以上、2つのケースを紹介しました。私はこの他にも、ほとんどFire-And-Forgetな長命のスレッド処理(アプリケーションと寿命がほぼ同じになるように作る1個だけのスレッドループ)をCancellationToeknSource
でキャンセルする場合に、Cancel()
メソッド実行後に長命スレッドのTask
をWait
することが多いです。このようなTask
はアプリを終了させるときとかリトライで長命スレッドを即復活させるときにしかキャンセルしないため、キャンセル処理全体が同期処理になりがちなのでWait
してOKなためです。
「Task
はWait()
やResult
プロパティにアクセスしてはいけない」という説明はよくありますし私も全面的に同意です。ただしそれは通常の場合であってよくわかって使うならOKです。それが適切なケースならば適切なやり方を行いましょう。ただし、急にTask.Wait()
されるとぎょっとするので**「終了処理のため同期的に行う」みたいなコメントがあると大変助かります**。
そもそも終了時に行わなければいけない処理なら、それが完了するまで正常終了させることはできません。終了時に行う処理は同期処理として実装するかTask
が完全に終わるまでWait()
で同期的に待ちましょう。複数のTask
を終了させる場合はTask.WhenAll(~~).Wait()
すれば並列したうえで同期的に待てばOKです。
ただしIsCancellationRequested
でキャンセルを判定してキャンセルされていたら終了処理する、みたいな場合にその中でawait
しているとデッドロックを起こす可能性があります。初期化処理や終了時処理はセンシティブなのでよく考えてプログラミングしましょう。
-
正確にはエントリポイントの
Run
メソッドの次に行く ↩