1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

非同期処理を含む終了処理はTaskをawaitせずにWait()で同期的に待たなければならないケースもあるよ

Posted at

非同期処理を含む終了処理

特にアンマネージリソースを扱う場合において、終了処理(キャンセルを含みます)では非同期処理を行うことがよくあります。

  • IAsyncDisposableインターフェイスを実装するクラス
  • System.Threading.ChannelsクラスのCompleteAsync()メソッドのような、適切なタイミングで明示的に実行する終了処理
  • アンマネージリソースを使って終了時のログを書き込む場合に1回だけ行うWriteAsync的な普通の非同期メソッドの実行
  • RDB処理が終わったときに行うトランザクションの後片付け

最近のAPIにはAsyncメソッドが標準搭載されているものも多いため、終了イベントに行うメソッドをasyncにしてawaitしたくなります。しかし、一部の終了処理ではawaitすると処理の完了を待たずに終了してしまうこともあり、ハマることがあります。
本稿ではawaitしてはいけない処理を2つ紹介します。

WinFormsFormClosingイベントおよび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()メソッド実行後に長命スレッドのTaskWaitすることが多いです。このようなTaskはアプリを終了させるときとかリトライで長命スレッドを即復活させるときにしかキャンセルしないため、キャンセル処理全体が同期処理になりがちなのでWaitしてOKなためです。

TaskWait()Resultプロパティにアクセスしてはいけない」という説明はよくありますし私も全面的に同意です。ただしそれは通常の場合であってよくわかって使うならOKです。それが適切なケースならば適切なやり方を行いましょう。ただし、急にTask.Wait()されるとぎょっとするので**「終了処理のため同期的に行う」みたいなコメントがあると大変助かります**。

そもそも終了時に行わなければいけない処理なら、それが完了するまで正常終了させることはできません。終了時に行う処理は同期処理として実装するかTaskが完全に終わるまでWait()で同期的に待ちましょう。複数のTaskを終了させる場合はTask.WhenAll(~~).Wait()すれば並列したうえで同期的に待てばOKです。
ただしIsCancellationRequestedでキャンセルを判定してキャンセルされていたら終了処理する、みたいな場合にその中でawaitしているとデッドロックを起こす可能性があります。初期化処理や終了時処理はセンシティブなのでよく考えてプログラミングしましょう。

  1. 正確にはエントリポイントのRunメソッドの次に行く

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?