web-dev-qa-db-ja.com

asp.net mvcの非同期メソッドを起動して忘れる

herehere tofire-and-forgetの質問などの一般的な答えは、async/awaitを使用することではありませんが、代わりに同期メソッドを渡すTask.RunまたはTaskFactory.StartNewを使用します。
ただし、fire-and-forgetにしたいメソッドが非同期であり、同等の同期メソッドがない場合があります。

更新ノート/警告:スティーブンクリアリーが以下に指摘したように、応答を送信した後、リクエストの作業を続けることは危険です。その理由は、その作業の進行中にAppDomainがシャットダウンされる可能性があるためです。詳細については、彼の回答のリンクを参照してください。とにかく、私は先にそれを指摘したかったので、間違った道を誰も送らないようにしました。

実際の作業は別のシステム(別のサーバー上の別のコンピューター)で行われるため、私のケースは有効だと思います。例外がある場合、サーバーまたはユーザーがそれに対してできることは何もなく、ユーザーに影響はありません。例外ログを参照し、手動でクリーンアップする(または自動化されたメカニズムを実装する)だけです。 AppDomainがシャットダウンされた場合、リモートシステムに残りのファイルがありますが、通常のメンテナンスサイクルの一部としてそれを取得します。その存在はWebサーバー(データベース)によって認識されず、その名前は一意です。タイムスタンプが付いているため、問題が発生することはありません。

Stephen Clearyが指摘したように、永続化メカニズムにアクセスできれば理想的ですが、残念ながら現時点ではそうではありません。

リクエストを開いたまま、クライアント側(javascript)でDeleteFooリクエストが正常に完了したふりをすることを検討しましたが、続行するには応答に情報が必要であるため、保持されます。

だから、元の質問...

例えば:

//External library
public async Task DeleteFooAsync();

私のasp.net mvcコードでは、DeleteFooAsyncを完全な方法で呼び出したいと思います-DeleteFooAsyncが完了するのを待っている応答を保持したくありません。何らかの理由でDeleteFooAsyncが失敗する(または例外をスローする)場合、ユーザーまたはプログラムがそれに対してできることは何もないので、エラーを記録したいだけです。

今、私は例外が観察されない例外になることを知っているので、私が考えることができる最も簡単なケースは次のとおりです:

//In my code
Task deleteTask = DeleteFooAsync()

//In my App_Start
TaskScheduler.UnobservedTaskException += ( sender, e ) =>
{
    m_log.Debug( "Unobserved exception! This exception would have been unobserved: {0}", e.Exception );
    e.SetObserved();
};

これを行う際にリスクはありますか?

私が考えることができる他のオプションは、次のような独自のラッパーを作成することです。

private void async DeleteFooWrapperAsync()
{
    try
    {
        await DeleteFooAsync();
    }
    catch(Exception exception )
    {
        m_log.Error("DeleteFooAsync failed: " + exception.ToString());
    }
}

次に、TaskFactory.StartNewで呼び出します(おそらく非同期アクションでラップします)。ただし、これは、非同期メソッドを呼び出して忘れる方法で呼び出すたびに、多くのラッパーコードのように見えます。

私の質問は、非同期メソッドを呼び出して忘れる方法で呼び出す正しい方法は何ですか?

UPDATE:

さて、コントローラーで次のことがわかりました(待機中の他の非同期呼び出しがあるため、コントローラーアクションを非同期にする必要はありません)。

[AcceptVerbs( HttpVerbs.Post )]
public async Task<JsonResult> DeleteItemAsync()
{
    Task deleteTask = DeleteFooAsync();
    ...
}

次の形式の例外が発生しました。

未処理の例外:System.NullReferenceException:オブジェクト参照がオブジェクトのインスタンスに設定されていません。 System.Web.ThreadContext.AssociateWithCurrentThread(BooleansetImpersonationContext)で

これは ここ で説明されており、SynchronizationContextと「すべての非同期作業が完了する前に返されたタスクが端末状態に遷移した」と関係があるようです。

したがって、機能する唯一の方法は次のとおりです。

Task foo = Task.Run( () => DeleteFooAsync() );

これが機能する理由についての私の理解は、StartNewがDeleteFooAsyncが動作するための新しいスレッドを取得するためです。

残念なことに、以下のScottの提案は、この場合の例外の処理には機能しません。fooはDeleteFooAsyncタスクではなく、Task.Runからのタスクであるため、DeleteFooAsyncからの例外を処理しないためです。私のUnobservedTaskExceptionは最終的に呼び出されるので、少なくともそれはまだ動作します。

だから、私は疑問がまだ残っていると思います、asp.net mvcで非同期メソッドをどのように発射して忘れるのですか?

52
acarlon

まず最初に、ASP.NETアプリケーションでは「火事と忘却」がほとんどの間違いであることを指摘させてください。 「Fire and forget」は、DeleteFooAsyncが実際に完了するかどうかを気にしない場合にのみ受け入れられるアプローチです。

その制限を受け入れてくれるなら、私は 私のブログにあるコード を持っています。これはタスクをASP.NETランタイムに登録し、同期作業と非同期作業の両方を受け入れます。

例外をログに記録するためのワンタイムラッパーメソッドを次のように作成できます。

private async Task LogExceptionsAsync(Func<Task> code)
{
  try
  {
    await code();
  }
  catch(Exception exception)
  {
    m_log.Error("Call failed: " + exception.ToString());
  }
}

そして、私のブログのBackgroundTaskManagerを次のように使用します。

BackgroundTaskManager.Run(() => LogExceptionsAsync(() => DeleteFooAsync()));

または、TaskScheduler.UnobservedTaskExceptionそして次のように呼び出すだけです:

BackgroundTaskManager.Run(() => DeleteFooAsync());
50
Stephen Cleary

.NET 4.5.2では、次のことができます

HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken => await LongMethodAsync());

ただし、ASP.NETドメイン内でのみ機能します

HostingEnvironment.QueueBackgroundWorkItemメソッドを使用すると、小さなバックグラウンド作業項目をスケジュールできます。 ASP.NETはこれらのアイテムを追跡し、すべてのバックグラウンド作業アイテムが完了するまでIISがワーカープロセスを突然終了するのを防ぎます。このメソッドはASP.NET管理アプリドメインの外部では呼び出すことができません。

詳細: https://msdn.Microsoft.com/en-us/library/ms171868(v = vs.110).aspx#v452

14
Korayem

これを処理する最良の方法は、 ContinueWith メソッドを使用して OnlyOnFaulted オプションを渡すことです。

private void button1_Click(object sender, EventArgs e)
{
    var deleteFooTask = DeleteFooAsync();
    deleteFooTask.ContinueWith(ErrorHandeler, TaskContinuationOptions.OnlyOnFaulted);
}

private void ErrorHandeler(Task obj)
{
    MessageBox.Show(String.Format("Exception happened in the background of DeleteFooAsync.\n{0}", obj.Exception));
}

public async Task DeleteFooAsync()
{
    await Task.Delay(5000);
    throw new Exception("Oops");
}

メッセージボックスを配置する場所には、ロガーを配置します。

8