2

IIS でタスクの実行が継続されないというかなり奇妙なケースに遭遇しましたawait(IIS に関連しているかどうかはわかりません)。Azure Storage と次のコントローラーを使用してこの問題を再現しました ( github の完全なソリューション)。

public class HomeController : Controller
{
    private static int _count;

    public ActionResult Index()
    {
        RunRequest(); //I don't want to wait on this task
        return View(_count);
    }

    public async Task RunRequest()
    {
        CloudStorageAccount account = CloudStorageAccount.DevelopmentStorageAccount;
        var cloudTable = account.CreateCloudTableClient().GetTableReference("test");

        Interlocked.Increment(ref _count);
        await Task.Factory.FromAsync<bool>(cloudTable.BeginCreateIfNotExists, cloudTable.EndCreateIfNotExists, null);

        Trace.WriteLine("This part of task after await is never executed");
        Interlocked.Decrement(ref _count);
    }
}

の値は常に 1 であると予想されますが (ビューでレンダリングされる場合)、F5 キーを数回押すと、更新のたびに値が増加_countすることがわかります。_countつまり、何らかの理由で継続が呼び出されないということです。

実際、私は少し嘘をつきました。最初にIndexが呼び出されたときに、継続が 1 回呼び出されることに気付きました。以降のすべての F5 は、カウンターを減少させません。

メソッドを非同期に変更した場合:

    public async Task<ActionResult> Index()
    {
        await RunRequest(); //I don't want to wait on this task
        return View(_count);
    }

非同期操作が完了するまでクライアントを待たせたくないことを除いて、すべてが期待どおりに機能し始めます。

したがって、私の質問は次のとおりです。なぜこれが起こるのか、できれば新しいスレッドにまたがることなく、「ファイアアンドフォーゲット」作業を実行する一貫した方法は何かを理解したいと思います。

4

3 に答える 3

3

「ファイア アンド フォーゲット」作業を実行するための一貫した方法は何ですか

ASP.NET は、ファイア アンド フォーゲットの作業用に設計されていません。HTTP リクエストを処理するように設計されています。HTTP 応答が生成されると (アクションが返されると)、その要求/応答サイクルが完了します。

ASP.NET は、アクティブな要求がない場合はいつでも AppDomain を自由に削除できることに注意してください。これは通常、非アクティブ タイムアウトの後、または AppDomain に一定数のガベージ コレクションがある場合、またはまったく理由もなく 29 時間ごとに、共有ホストで行われます。

したがって、「起動して忘れる」ことは本当に必要ありません。応答を生成したいが、ASP.NET にそれを忘れさせたくないのです。willの簡単な解決策はConfigureAwait(false)、誰もがそれを忘れてしまう原因となります。

このテーマについて詳しく説明しているブログ記事があります。つまり、応答が生成されるに、永続的なレイヤー (Azure テーブルなど) で実行される作業を記録する必要があります。それが理想的なソリューションです。

理想的な解決策を実行しないと、危険な生活を送ることになります。私のブログ投稿にはTask、ASP.NET ランタイムに s を登録するコードがあります。これにより、応答を早期に返すことができますが、実際にはまだ完了していないことを ASP.NET に通知できます。これにより、未処理の作業中に ASP.NET がサイトを停止するのを防ぐことができますが、ハード ドライブのクラッシュやサーバーの電源コードにつまずくなど、より根本的な障害から保護することはできません。

私のブログ投稿のコードを以下に複製します。AsyncCountdownEventそれは私のAsyncEx ライブラリに依存します:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Hosting;
using Nito.AsyncEx;

/// <summary>
/// A type that tracks background operations and notifies ASP.NET that they are still in progress.
/// </summary>
public sealed class BackgroundTaskManager : IRegisteredObject
{
    /// <summary>
    /// A cancellation token that is set when ASP.NET is shutting down the app domain.
    /// </summary>
    private readonly CancellationTokenSource shutdown;

    /// <summary>
    /// A countdown event that is incremented each time a task is registered and decremented each time it completes. When it reaches zero, we are ready to shut down the app domain. 
    /// </summary>
    private readonly AsyncCountdownEvent count;

    /// <summary>
    /// A task that completes after <see cref="count"/> reaches zero and the object has been unregistered.
    /// </summary>
    private readonly Task done;

    private BackgroundTaskManager()
    {
        // Start the count at 1 and decrement it when ASP.NET notifies us we're shutting down.
        shutdown = new CancellationTokenSource();
        count = new AsyncCountdownEvent(1);
        shutdown.Token.Register(() => count.Signal(), useSynchronizationContext: false);

        // Register the object and unregister it when the count reaches zero.
        HostingEnvironment.RegisterObject(this);
        done = count.WaitAsync().ContinueWith(_ => HostingEnvironment.UnregisterObject(this), TaskContinuationOptions.ExecuteSynchronously);
    }

    void IRegisteredObject.Stop(bool immediate)
    {
        shutdown.Cancel();
        if (immediate)
            done.Wait();
    }

    /// <summary>
    /// Registers a task with the ASP.NET runtime.
    /// </summary>
    /// <param name="task">The task to register.</param>
    private void Register(Task task)
    {
        count.AddCount();
        task.ContinueWith(_ => count.Signal(), TaskContinuationOptions.ExecuteSynchronously);
    }

    /// <summary>
    /// The background task manager for this app domain.
    /// </summary>
    private static readonly BackgroundTaskManager instance = new BackgroundTaskManager();

    /// <summary>
    /// Gets a cancellation token that is set when ASP.NET is shutting down the app domain.
    /// </summary>
    public static CancellationToken Shutdown { get { return instance.shutdown.Token; } }

    /// <summary>
    /// Executes an <c>async</c> background operation, registering it with ASP.NET.
    /// </summary>
    /// <param name="operation">The background operation.</param>
    public static void Run(Func<Task> operation)
    {
        instance.Register(Task.Run(operation));
    }

    /// <summary>
    /// Executes a background operation, registering it with ASP.NET.
    /// </summary>
    /// <param name="operation">The background operation.</param>
    public static void Run(Action operation)
    {
        instance.Register(Task.Run(operation));
    }
}

asyncまたは同期コードに対して次のように使用できます。

BackgroundTaskManager.Run(() =>
{
    // Synchronous example
    Thread.Sleep(20000);
});
BackgroundTaskManager.Run(async () =>
{
    // Asynchronous example
    await Task.Delay(20000);
});
于 2012-12-21T16:16:52.327 に答える
2

まあ、どこかに継続を実行するスレッドが必要です。問題は、 awaiter でキャプチャされたコンテキストが、リクエストが既に終了していることを「認識」していることだと思います。その状況で何が起こるかの詳細はわかりませんが、継続を無視するだけの可能性があります。確かに、それは少し奇妙に聞こえます...

あなたは使用してみることができます:

await Task.Factory.FromAsync<bool>(cloudTable.BeginCreateIfNotExists,
                                   cloudTable.EndCreateIfNotExists, null)
          .ConfigureAwait(false);

そうすれば、キャプチャされたコンテキストを続行しようとせず、代わりに任意のスレッドプール スレッドを続行しようとします。役に立たないかもしれませんが、試してみる価値はあります。

于 2012-12-21T15:34:02.627 に答える
2

問題はawait、継続が最初に開始された同期コンテキストで実行されるように構成することです。これは正直なところ、この機能の便利な側面の 1 つですが、この場合は問題になります。ここでは、ビューを返しているため、継続が起動するまでに同期コンテキストは存在しません。

私の推測では、既に「完了」している同期コンテキストにアクセスしようとすると、例外がスローされるため、コードが機能しないのです。

ConfigureAwait(false)メソッドの最後に追加するFromAsyncと、スレッドプールスレッドで実行できるようになります。これは、あなたの場合には問題ありません。

その他のオプションは、タスクから例外がスローされているか、非同期操作がまったく終了していないことです。

于 2012-12-21T15:34:29.523 に答える