問題
シンプルな web-service を含む ASP.NET 4.0 WebForms ページがありますWebMethod
。このメソッドは、非同期/TPL コードへの同期ラッパーとして機能します。私が直面している問題は、内部Task
に null SynchronizationContext
(私の好み) がある場合があることですが、同期コンテキストがSystem.Web.LegacyAspNetSynchronizationContext
. 私が提供した例では、これは実際には問題を引き起こしませんが、実際の開発シナリオではデッドロックにつながる可能性があります。
サービスへの最初の呼び出しは常に null 同期コンテキストで実行されるように見えますが、次のいくつかもそうなる可能性があります。しかし、いくつかの急速な要求が発生すると、ASP.NET 同期コンテキストにポップし始めます。
コード
[WebMethod]
public static string MyWebMethod(string name)
{
var rnd = new Random();
int eventId = rnd.Next();
TaskHolder holder = new TaskHolder(eventId);
System.Diagnostics.Debug.WriteLine("Event Id: {0}. Web method thread Id: {1}",
eventId,
Thread.CurrentThread.ManagedThreadId);
var taskResult = Task.Factory.StartNew(
function: () => holder.SampleTask().Result,
creationOptions: TaskCreationOptions.None,
cancellationToken: System.Threading.CancellationToken.None,
scheduler: TaskScheduler.Default)
.Result;
return "Hello " + name + ", result is " + taskResult;
}
存在の定義TaskHolder
:
public class TaskHolder
{
private int _eventId;
private ProgressMessageHandler _prg;
private HttpClient _client;
public TaskHolder(int eventId)
{
_eventId = eventId;
_prg = new ProgressMessageHandler();
_client = HttpClientFactory.Create(_prg);
}
public Task<string> SampleTask()
{
System.Diagnostics.Debug.WriteLine("Event Id: {0}. Pre-task thread Id: {1}",
_eventId,
Thread.CurrentThread.ManagedThreadId);
return _client.GetAsync("http://www.google.com")
.ContinueWith((t) =>
{
System.Diagnostics.Debug.WriteLine("Event Id: {0}. Continuation-task thread Id: {1}",
_eventId,
Thread.CurrentThread.ManagedThreadId);
t.Wait();
return string.Format("Length is: {0}", t.Result.Content.Headers.ContentLength.HasValue ? t.Result.Content.Headers.ContentLength.Value.ToString() : "unknown");
}, scheduler: TaskScheduler.Default);
}
}
分析
私の理解でTaskScheduler.Default
は、それはThreadPool
スケジューラです。つまり、スレッドは ASP.NET スレッドに到達しません。この記事によると、「タスク並列ライブラリと PLINQ の既定のスケジューラは、.NET Framework ThreadPool を使用して作業をキューに入れ、実行します」。それに基づいて、SynchronizationContext
内部SampleTask
は常にnullになると思います。
さらに、私の理解ではSampleTask
、 ASP.NET 上にある場合、 inSynchronizationContext
への呼び出しがデッドロックする可能性があります。.Result
MyWebMethod
「完全に非同期」にするつもりはないので、これは「同期オン非同期」シナリオです。Stephen Toub によるこの記事によると、「「非同期ではなく同期」が本当に必要な場合はどうすればよいでしょうか? というタイトルのセクションにあります。次のコードは安全なラッパーである必要があります。
Task.Run(() => holder.SampleTask()).Result
同じく Stephen Toub によるこの別の記事によると、上記は機能的に次のものと同等である必要があります。
Task.Factory.StartNew(
() => holder.SampleTask().Result,
CancellationToken.None,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default);
.NET 4.0 を使用しているため、.NET にアクセスできません。これは私の問題だと思いましTaskCreationOptions.DenyChildAttach
た。しかし、同じサンプルを .NET 4.5 で実行して切り替えたところTaskCreationOptions.DenyChildAttach
、同じように動作しました (ASP.NET 同期コンテキストを取得する場合があります)。
そこで、「元の」推奨事項に近づき、.NET 4.5 で実装することにしました。
Task.Run(() => holder.SampleTask()).Result
そして、これは常に null 同期コンテキストを持つという点で機能します。Task.Run と Task.Factory.StartNew の記事が間違っていることを示唆しているのはどれですか?
実用的なアプローチは、.NET 4.5 にアップグレードしてTask.Run
実装を使用することですが、それには開発時間が必要になるため、(可能であれば) もっと差し迫った問題に費やしたいと思います。さらにTaskScheduler
、さまざまなシナリオで何が起こっているのかを把握したいと思いTaskCreationOptions
ます。
.NET 4.0 では、思い通りに動作しているように見えることを偶然発見しましたTaskCreationOptions.PreferFairness
(すべての実行で同期コンテキストが null になっています)。シナリオ)。
編集
いくつかの追加情報...デッドロックを行うコードでサンプルコードを更新し、タスクが実行されているスレッドを示すデバッグ出力を含めました。プレタスクまたは継続タスクの出力が WebMethod と同じスレッド ID を示している場合、デッドロックが発生します。
不思議なことに、ProgressMessageHandler を使用しないと、デッドロックを再現できないようです。私の印象では、これは問題ではなく、下流のコードに関係なく、適切なメソッドTask.Factory.StartNew
またはTask.Run
メソッドを使用して非同期メソッドを同期コンテキストに安全に「ラップ」できるはずだというものでした。しかし、これはそうではないようです?