私はasync/awaitを多用する新しいコードベースを使用しています。私のチームのほとんどの人は、非同期/待機もかなり新しいです。私たちは一般的に Microsoftによって指定されたベストプラクティス を保持する傾向がありますが、非同期呼び出しを通過するためにコンテキストが必要であり、ConfigureAwait(false)
を実行しないライブラリを操作しています。
これらすべてを組み合わせると、記事で説明されている非同期デッドロックが発生します...毎週。モックされたデータソース(通常はTask.FromResult
経由)ではデッドロックをトリガーするのに十分でないため、ユニットテスト中には表示されません。そのため、実行時テストまたは統合テスト中に、一部のサービスコールは昼食に出かけ、二度と戻りません。これはサーバーを殺し、一般に混乱を招きます。
問題は、ミスが発生した場所を追跡する(通常は、完全に非同期ではない)には、通常、手動のコード検査が必要であり、時間がかかり、自動化できないことです。
デッドロックの原因を診断するより良い方法は何ですか?
わかりました-次のことがあなたに役立つかどうかはわかりません。私はあなたの場合に当てはまるかもしれないしそうでないかもしれないソリューションを開発する際にいくつかの仮定を立てたからです。たぶん、私の「解決策」は理論的すぎて、人工的な例でのみ機能します-以下のもの以外のテストは行っていません。
さらに、以下は実際の解決策よりも回避策が多いと思いますが、応答がないことを考えると、それでも何もないよりはましだと思います(解決策を待っていましたが、解決策が見られませんでした投稿されて、私は問題をいじり始めました)。
しかし十分言った:整数を取得するために使用できる単純なデータサービスがあるとしましょう:
public interface IDataService
{
Task<int> LoadMagicInteger();
}
簡単な実装では、非同期コードを使用します。
public sealed class CustomDataService
: IDataService
{
public async Task<int> LoadMagicInteger()
{
Console.WriteLine("LoadMagicInteger - 1");
await Task.Delay(100);
Console.WriteLine("LoadMagicInteger - 2");
var result = 42;
Console.WriteLine("LoadMagicInteger - 3");
await Task.Delay(100);
Console.WriteLine("LoadMagicInteger - 4");
return result;
}
}
ここで、このクラスで示されているようにコードを「誤って」使用している場合、問題が発生します。 Foo
がawait
のように結果をBar
ingする代わりにTask.Result
に誤ってアクセスする:
public sealed class ClassToTest
{
private readonly IDataService _dataService;
public ClassToTest(IDataService dataService)
{
this._dataService = dataService;
}
public async Task<int> Foo()
{
var result = this._dataService.LoadMagicInteger().Result;
return result;
}
public async Task<int> Bar()
{
var result = await this._dataService.LoadMagicInteger();
return result;
}
}
私たち(あなた)が今必要としているのは、Bar
を呼び出すと成功し、Foo
を呼び出すと失敗するテストを作成する方法です(少なくとも質問が正しく理解されていれば、;-))。
コードに話させます。これが私が思いついたものです(Visual Studioテストを使用していますが、NUnitを使用しても動作するはずです):
DataServiceMock
はTaskCompletionSource<T>
を使用します。これにより、次のテストにつながるテスト実行の定義されたポイントに結果を設定できます。デリゲートを使用してTaskCompletionSourceをテストに戻していることに注意してください。これをテストのInitializeメソッドに入れて、プロパティを使用することもできます。
TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;
Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());
tcs.TrySetResult(42);
var result = task.Result;
Assert.AreEqual(42, result);
this._end = true;
ここで起こっていることは、まずブロックせずにメソッドを残せることを確認することです(誰かがTask.Result
にアクセスした場合これは機能しません-この場合、タスクの結果が利用可能になるまでタイムアウトに陥るでしょうメソッドが戻った後)。
次に、結果を設定し(メソッドが実行できるようになりました)、結果を確認します(単体テスト内で、実際にTask.Resultにアクセスできますwant発生するブロッキング)。 。
完全なテストクラス-BarTest
は成功し、FooTest
は必要に応じて失敗します。
[TestClass]
public class UnitTest1
{
private DataServiceMock _dataService;
private ClassToTest _instance;
private bool _end;
[TestInitialize]
public void Initialize()
{
this._dataService = new DataServiceMock();
this._instance = new ClassToTest(this._dataService);
this._end = false;
}
[TestCleanup]
public void Cleanup()
{
Assert.IsTrue(this._end);
}
[TestMethod]
public void FooTest()
{
TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;
Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());
tcs.TrySetResult(42);
var result = task.Result;
Assert.AreEqual(42, result);
this._end = true;
}
[TestMethod]
public void BarTest()
{
TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;
Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Bar());
tcs.TrySetResult(42);
var result = task.Result;
Assert.AreEqual(42, result);
this._end = true;
}
}
そして、デッドロック/タイムアウトをテストするための小さなヘルパークラス:
public static class TaskTestHelper
{
public static void AssertDoesNotBlock(Action action, int timeout = 1000)
{
var timeoutTask = Task.Delay(timeout);
var task = Task.Factory.StartNew(action);
Task.WaitAny(timeoutTask, task);
Assert.IsTrue(task.IsCompleted);
}
}