web-dev-qa-db-ja.com

ASP.NET Web APIで非スレッドセーフの非同期/待機APIおよびパターンを使用する方法

この質問は EF Data Context-Async/Await&Multithreading によってトリガーされました。私はそれに答えましたが、究極の解決策を提供していません。

元の問題は、Microsoft Entity Frameworkの DbContext などの便利な.NET APIがたくさんあり、awaitで使用するように設計された非同期メソッドを提供しているにもかかわらず、スレッドセーフではありません。そのため、デスクトップUIアプリでの使用には最適ですが、サーバーサイドアプリでは使用できません。 [編集済み]これは実際にはDbContextには適用されない可能性があります。Microsoftの EF6スレッドセーフティに関するステートメント =、自分で判断してください。[/ EDITED]

OperationContextScope でWCFサービスプロキシを呼び出すなど、同じカテゴリに分類されるいくつかの確立されたコードパターンもあります(質問 here および here )。たとえば、 :

using (var docClient = CreateDocumentServiceClient())
using (new OperationContextScope(docClient.InnerChannel))
{
    return await docClient.GetDocumentAsync(docId);
}

OperationContextScopeはその実装でスレッドローカルストレージを使用するため、これは失敗する可能性があります。

問題の原因はAspNetSynchronizationContextで、非同期ASP.NETページで使用され、より少ないスレッドでより多くのHTTPリクエストを処理しますASP.NETスレッドプール。 AspNetSynchronizationContextを使用すると、非同期操作を開始したスレッドとは別のスレッドでawaitの継続をキューに入れることができ、元のスレッドはプールに解放され、別のHTTPリクエストを処理するために使用できます。 これにより、サーバー側のコードのスケーラビリティが大幅に向上します。このメカニズムについて詳しく説明します It's All About the SynchronizationContext 、a必読。したがって、同時APIアクセスはありませんが、潜在的なスレッド切り替えにより、前述のAPIを使用できなくなります。

スケーラビリティを犠牲にすることなくこれを解決する方法を考えていました。どうやら、これらのAPIを元に戻す唯一の方法はスレッド切り替えの影響を受ける可能性のある非同期呼び出しのスコープについて、スレッドアフィニティを維持します

そのようなスレッド親和性があるとしましょう。とにかく、それらの呼び出しのほとんどはIOバインドです( スレッドはありません )。非同期タスクが保留中の間、それが発生したスレッドは、別の同様のタスクの継続を処理するために使用でき、その結果はすでに利用可能です。したがって、スケーラビリティをあまり損なわないはずです。このアプローチは新しいものではありません。実際、同様のシングルスレッドモデルは Node.jsで正常に使用されます。 IMO、これはNode.jsを非常に人気にするものの1つです。

このアプローチがASP.NETコンテキストで使用できなかった理由がわかりません。カスタムタスクスケジューラ(ThreadAffinityTaskSchedulerと呼びます)は、「アフィニティアパートメント」スレッドの個別のプールを維持して、スケーラビリティをさらに向上させる場合があります。タスクがこれらの「アパートメント」スレッドの1つにキューイングされると、タスク内のすべてのawait継続がまったく同じスレッドで行われます。

リンクされた質問 のスレッドセーフでないAPIが、このようなThreadAffinityTaskSchedulerでどのように使用されるかを次に示します。

// create a global instance of ThreadAffinityTaskScheduler - per web app
public static class GlobalState 
{
    public static ThreadAffinityTaskScheduler TaScheduler { get; private set; }

    public static GlobalState 
    {
        GlobalState.TaScheduler = new ThreadAffinityTaskScheduler(
            numberOfThreads: 10);
    }
}

// ...

// run a task which uses non-thread-safe APIs
var result = await GlobalState.TaScheduler.Run(() => 
{
    using (var dataContext = new DataContext())
    {
        var something = await dataContext.someEntities.FirstOrDefaultAsync(e => e.Id == 1);
        var morething = await dataContext.someEntities.FirstOrDefaultAsync(e => e.Id == 2);
        // ... 
        // transform "something" and "morething" into thread-safe objects and return the result
        return data;
    }
}, CancellationToken.None);

私は先に進み、実装されたThreadAffinityTaskScheduler の証明として、Stephen Toubの優れた StaTaskScheduler に基づいて =。 ThreadAffinityTaskSchedulerによって維持されるプールスレッドは、従来のCOMの意味ではSTAスレッドではありませんが、awaitの継続に対するスレッドアフィニティを実装します(SingleThreadSynchronizationContextがその責任を負います)。

これまでのところ、このコードをコンソールアプリとしてテストしましたが、設計どおりに動作するようです。 ASP.NETページ内ではまだテストしていません。私はプロダクションASP.NET開発の経験があまりないので、私の質問は次のとおりです。

  1. ASP.NETの非スレッドセーフAPIの単純なsynchronous呼び出しに対してこのアプローチを使用することは理にかなっていますか(主な目的はスケーラビリティの犠牲を避けることです) )?

  2. 同期API呼び出しを使用したり、これらのAPをまったく回避したりする以外に、代替アプローチはありますか?

  3. 誰かがASP.NET MVCまたはWeb APIプロジェクトで同様のものを使用し、彼/彼女の経験を共有する準備ができていますか?

  4. ASP.NETを使用してこのアプローチをストレステストおよびプロファイルする方法についてのアドバイスをいただければ幸いです。

34
noseratio

Entity Frameworkは、awaitポイントをまたがるスレッドジャンプを(適切に)処理します。そうでない場合は、EFのバグです。 OTOH、OperationContextScopeはTLSに基づいており、await- safeではありません。

1.同期APIはASP.NETコンテキストを維持します。これには、ユーザーの身元や文化など、処理中に重要になることが多いものが含まれます。また、いくつかのASP.NET APIは、実際のASP.NETコンテキストで実行されていることを前提としています(HttpContext.Currentを使用するだけではなく、SynchronizationContext.CurrentAspNetSynchronizationContextのインスタンスであると実際に想定しています)。 。

2-3。私は 自分のシングルスレッドコンテキスト ASP.NETコンテキスト内に直接ネストして、コードを複製する必要なく async MVC子アクションを機能させる を使用しようとしました。ただし、スケーラビリティの利点が失われるだけでなく(少なくとも要求のその部分について)、ASP.NETコンテキストで実行されていると想定して、ASP.NET APIにも実行されます。

したがって、私はこのアプローチを実稼働で使用したことはありません。必要なときに同期APIを使用するだけです。

9
Stephen Cleary

マルチスレッドと非同期を絡み合わせてはいけません。オブジェクトがスレッドセーフでない場合の問題は、単一のインスタンス(または静的)が複数のスレッドによってアクセスされる場合です同時に。非同期呼び出しの場合、コンテキストはおそらく継続内の別のスレッドからアクセスされますが、同時にアクセスされることはありません(複数の要求間で共有されない場合は、最初から適切ではありません)。

2
riezebosch