非同期/待機デッドロックを診断するにはどうすればよいですか?


24

私はasync / awaitを多用する新しいコードベースで作業しています。私のチームのほとんどの人は、async / awaitにかなり慣れていません。私たちは一般的にMicrosoftによって指定されたベストプラクティスを保持する傾向がありますが、一般的には非同期呼び出しを通過するためにコンテキストが必要で、そうでないライブラリを使用していConfigureAwait(false)ます。

それらすべてを組み合わせて、毎週記事で説明されている非同期デッドロックに遭遇します。模擬テストされたデータソース(通常はを介してTask.FromResult)はデッドロックをトリガーするのに十分ではないため、ユニットテスト中には表示されません。そのため、実行時または統合テスト中に、一部のサービスコールは昼食に出て戻りません。それはサーバーを殺し、一般的に混乱をもたらします。

問題は、間違いがどこで発生したかを追跡すること(通常は完全に非同期ではないこと)には、通常、手作業のコード検査が含まれ、時間がかかり、自動化できないことです。

デッドロックの原因を診断するより良い方法は何ですか?


1
良い質問; 私はこれを自分で考えました。この男のasync記事のコレクションを読んだことがありますか?
ロバートハーヴェイ

@RobertHarvey-すべてではないかもしれませんが、私はいくつか読みました。詳細「これらの2つまたは3つのことをどこでも行うようにしてください。さもないと、実行時にコードが恐ろしく死にます」。
テラスティン

非同期の削除またはその使用を最も有益なポイントまで減らすことを受け入れていますか?非同期IOはすべてではありません。
usr

1
デッドロックを再現できる場合は、スタックトレースだけでブロッキング呼び出しを確認できませんか?
svick

2
問題が「完全に非同期ではない」場合、それは、デッドロックの半分が従来のデッドロックであり、同期コンテキストスレッドのスタックトレースに表示されることを意味します。
svick

回答:


4

わかりました-あなたの場合に当てはまるかもしれないし、そうでないかもしれないソリューションを開発する際にいくつかの仮定をしたので、私は以下があなたにとって助けになるかどうかわかりません。たぶん私の「解決策」はあまりにも理論的であり、人工的な例のためにのみ機能します-私は以下のもの以外のテストを行っていません。
さらに、実際の解決策よりも次の回避策がありますが、応答がないことを考えると、何もしないよりはましだと思います(解決策を待っているあなたの質問を見続けましたが、投稿された質問は見ていませんでした問題の周り)。

しかし、十分に言った:整数を取得するために使用できる単純なデータサービスがあるとします。

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次のように結果にアクセスするTask.Result代わりに、誤ってアクセスします。awaitBar

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にアクセスできます)。

完全なテストクラス- 必要に応じて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);
    }
}

いい答えだ。時間があれば自分でコードを試してみるつもりです(実際に動作するかどうかは確かにわかりません)が、その努力に対する称賛と賛成です。
ロバートハーヴェイ

-2

これは、巨大で非常にマルチスレッドのアプリケーションで使用した戦略です。

まず、(残念ながら)ミューテックス周辺のデータ構造が必要であり、同期呼び出しディレクトリを作成しません。そのデータ構造には、以前にロックされたミューテックスへのリンクがあります。すべてのミューテックスには、0から始まる「レベル」があります。これは、ミューテックスの作成時に割り当てられ、変更することはできません。

そして、ルールは次のとおりです。ミューテックスがロックされている場合、他のミューテックスのみを下位レベルでロックする必要があります。そのルールに従えば、デッドロックは発生しません。違反が見つかっても、アプリケーションは正常に動作しています。

違反を見つけた場合、2つの可能性があります。レベルの割り当てが間違っている可能性があります。AをロックしてからBをロックしたので、Bのレベルは低くなります。そのため、レベルを修正して再試行します。

他の可能性:あなたはそれを修正することはできません。いくつかのコードはAをロックしてからBをロックし、他のコードはBをロックしてからAをロックします。これを許可するレベルを割り当てる方法はありません。そしてもちろん、これは潜在的なデッドロックです。両方のコードが異なるスレッドで同時に実行される場合、デッドロックの可能性があります。

これを導入した後、レベルを調整する必要があるかなり短いフェーズがあり、その後、潜在的なデッドロックが見つかった長いフェーズがありました。


4
すみません、それは非同期/待機動作にどのように適用されますか?カスタムミューテックス管理構造をタスクパラレルライブラリに現実的に挿入することはできません。
テラスティン

-3

Async / Awaitを使用して、データベースのような高価な呼び出しを並列化できますか?DBの実行パスによっては、これが不可能な場合があります。

async / awaitを使用したテストカバレッジは困難な場合があり、バグを見つけるための実際の実稼働での使用とは異なります。考えられるパターンの1つは、相関IDを渡してスタックに記録し、エラーを記録するカスケードタイムアウトを設定することです。これはSOAパターンに近いものですが、少なくともそれがどこから来たのかを知ることができます。これをSplunkで使用して、デッドロックを見つけました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.