비동기 / 대기 교착 상태를 진단하려면 어떻게해야합니까?


24

async / await를 많이 사용하는 새로운 코드베이스로 작업하고 있습니다. 우리 팀의 사람들 대부분은 비동기 / 대기에 상당히 새로운 사람들입니다. 우리는 일반적으로 Microsoft가 지정한 모범 사례 를 따르는 경향이 있지만 일반적으로 비동기 호출을 통과하는 컨텍스트가 필요하며 그렇지 않은 라이브러리를 사용하고 ConfigureAwait(false)있습니다.

이 모든 것을 결합하면 매주 기사에 설명 된 비동기 교착 상태가 발생합니다. 우리의 조롱 된 데이터 소스 (보통을 통해 Task.FromResult)는 교착 상태를 유발하기에 충분하지 않기 때문에 단위 테스트 중에 표시 되지 않습니다. 따라서 런타임 또는 통합 테스트 중에 일부 서비스 요청은 점심 식사를 마치고 돌아 오지 않습니다. 그것은 서버를 죽이고 일반적으로 문제를 일으킨다.

문제는 실수가 발생한 위치 (일반적으로 비 동기화되지 않은)를 추적하는 데는 일반적으로 수동 코드 검사가 필요하며 이는 시간이 많이 걸리고 자동화 할 수 없다는 것입니다.

교착 상태의 원인을 진단하는 더 좋은 방법은 무엇입니까?


1
좋은 질문; 나는 이것을 스스로 궁금해했다. 이 사람의 async기사 모음 을 읽었 습니까?
Robert Harvey

@RobertHarvey-어쩌면 전부는 아니지만 일부를 읽었습니다. 더 많은 "이 두세 가지 일을 어디에서나 수행하십시오. 그렇지 않으면 코드가 런타임에 끔찍한 죽음으로 죽을 것입니다."
Telastyn

비동기식을 삭제하거나 가장 유리한 지점으로 사용량을 줄이려고하십니까? 비동기 IO는 전부가 아니거나 아무것도 아닙니다.
usr

1
교착 상태를 재현 할 수 있다면 스택 추적을보고 차단 호출을 볼 수 없습니까?
svick

2
문제가 "완전히 비동기 적이 지 않다"라면, 교착 상태의 절반이 전통적인 교착 상태이며 동기화 컨텍스트 스레드의 스택 추적에서 볼 수 있어야합니다.
svick

답변:


4

Ok-귀하의 경우에 맞거나 맞지 않을 수있는 솔루션을 개발할 때 약간의 가정을했기 때문에 다음이 귀하에게 도움이 될지 확실하지 않습니다. 어쩌면 내 "솔루션"이 너무 이론적이며 인공적인 예에서만 작동합니다. 아래 항목을 넘어서는 테스트를 수행하지 않았습니다.
또한 실제 솔루션보다 다음 해결 방법을 더 많이 볼 수 있지만 응답 부족을 고려하면 여전히 아무것도 아닌 것보다 낫다고 생각합니다 (문제를 기다리는 동안 질문을 계속 보았지만 게시 된 것을 보지 못했습니다) 문제와 함께).

그러나 충분히 말했다 : 정수를 검색하는 데 사용할 수있는 간단한 데이터 서비스가 있다고 가정 해 봅시다.

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);
    }
}

좋은 대답입니다. 시간이있을 때 직접 코드를 사용해 볼 계획입니다 (실제로 작동하는지 확실하지는 않지만).
Robert Harvey

-2

다음은 거대하고 매우 다중 스레드 응용 프로그램에서 사용한 전략입니다.

먼저, (불행히도) 뮤텍스에 대한 데이터 구조가 필요하며 동기화 호출 디렉토리를 만들지 마십시오. 이 데이터 구조에는 이전에 잠긴 뮤텍스에 대한 링크가 있습니다. 모든 뮤텍스는 0부터 시작하여 "레벨"을 가지며, 뮤텍스가 생성 될 때 할당하고 절대 변경할 수 없습니다.

그리고 규칙은 다음과 같습니다. 뮤텍스가 잠겨 있으면 다른 뮤텍스를 낮은 레벨에서만 잠 가야합니다. 이 규칙을 따르면 교착 상태를 가질 수 없습니다. 위반 사항을 발견 한 경우 응용 프로그램이 여전히 제대로 작동하고 있습니다.

위반을 발견하면 두 가지 가능성이 있습니다. 레벨을 잘못 할당했을 수 있습니다. A를 잠그고 B를 잠그면 B의 레벨이 낮아 졌을 것입니다. 따라서 레벨을 수정하고 다시 시도하십시오.

다른 가능성 : 고칠 수 없습니다. 일부 코드는 A를 잠그고 B를 잠그고 다른 코드는 B를 잠그고 A를 잠급니다.이를 허용하기 위해 레벨을 지정할 방법이 없습니다. 물론 이것은 잠재적 교착 상태입니다. 두 코드가 서로 다른 스레드에서 동시에 실행되면 교착 상태가 발생할 가능성이 있습니다.

이것을 도입 한 후, 레벨을 조정해야하는 다소 짧은 단계가 있었고 잠재적 인 교착 상태가 발견 된 단계가 더 길었습니다.


4
죄송합니다. 비동기 / 대기 동작에 어떻게 적용됩니까? 사용자 정의 뮤텍스 관리 구조를 작업 병렬 라이브러리에 현실적으로 주입 할 수 없습니다.
Telastyn

-3

Async / Await를 사용하여 데이터베이스와 같은 비싼 통화를 병렬화 할 수 있습니까? DB의 실행 경로에 따라 불가능할 수도 있습니다.

async / await를 사용한 테스트 범위는 까다로울 수 있으며 버그를 찾기위한 실제 프로덕션 사용량과 같은 것은 없습니다. 고려할 수있는 패턴 중 하나는 상관 관계 ID를 전달하고 스택에 기록한 다음 오류를 기록하는 계단식 시간 초과를 갖는 것입니다. 이것은 SOA 패턴에 대한 것이지만 적어도 그것이 어디에서 왔는지에 대한 감각을 줄 것입니다. 교착 상태를 찾기 위해 이것을 Splunk와 함께 사용했습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.