Como posso diagnosticar assíncrono / aguardar conflitos?


24

Estou trabalhando com uma nova base de código que faz uso pesado de async / waitit. A maioria das pessoas da minha equipe também é relativamente nova em assíncrona / aguardar. Geralmente, tendemos a manter as Práticas recomendadas conforme especificado pela Microsoft , mas geralmente precisamos que nosso contexto flua através da chamada assíncrona e esteja trabalhando com bibliotecas que não o fazem ConfigureAwait(false).

Combine todas essas coisas e encontraremos impasses assíncronos descritos no artigo ... semanalmente. Eles não aparecem durante o teste de unidade, porque nossas fontes de dados simuladas (geralmente via Task.FromResult) não são suficientes para acionar o impasse. Portanto, durante os testes de tempo de execução ou integração, algumas chamadas de serviço saem para almoçar e nunca mais retornam. Isso mata os servidores e geralmente atrapalha as coisas.

O problema é que rastrear onde o erro foi cometido (geralmente apenas não sendo assíncrono até o fim) geralmente envolve inspeção manual de código, que consome tempo e não pode ser automatizada.

Qual é a melhor maneira de diagnosticar o que causou o impasse?


11
Boa pergunta; Eu mesmo me perguntei isso. Você leu a coleção de asyncartigos desse cara ?
Robert Harvey

@ RobertHarvey - talvez não todos, mas eu já li alguns. Mais "Certifique-se de fazer essas duas / três coisas em qualquer lugar ou o seu código sofrerá uma morte horrível em tempo de execução".
Telastyn

Você está aberto a abandonar o assíncrono ou reduzir seu uso aos pontos mais benéficos? E / S assíncrona não é tudo ou nada.
usr

11
Se você pode reproduzir o impasse, não pode simplesmente olhar para o rastreamento da pilha para ver a chamada de bloqueio?
Svick # 04/15

2
Se o problema "não for totalmente assíncrono", isso significa que metade do conflito é um conflito tradicional e deve estar visível no rastreamento da pilha do encadeamento do contexto de sincronização.
Svick # 04/15

Respostas:


4

Ok - não tenho certeza se o seguinte será de alguma ajuda para você, porque fiz algumas suposições no desenvolvimento de uma solução que pode ou não ser verdadeira no seu caso. Talvez minha "solução" seja muito teórica e funcione apenas para exemplos artificiais - eu não realizei nenhum teste além dos itens abaixo.
Além disso, eu consideraria o seguinte mais uma solução alternativa do que uma solução real, mas, considerando a falta de respostas, acho que ainda pode ser melhor do que nada (continuei observando sua pergunta aguardando uma solução, mas não vendo uma sendo postada, comecei a jogar por aí com o problema).

Mas basta dizer: digamos que temos um serviço de dados simples que pode ser usado para recuperar um número inteiro:

public interface IDataService
{
    Task<int> LoadMagicInteger();
}

Uma implementação simples usa código assíncrono:

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

Agora, surge um problema, se estivermos usando o código "incorretamente", conforme ilustrado por esta classe. Fooacessa incorretamente, em Task.Resultvez de awaiting o resultado, como Bar:

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

O que precisamos agora é de uma maneira de escrever um teste que seja bem-sucedido ao ligar, Barmas falhe ao ligar Foo(pelo menos se eu entendi a pergunta corretamente ;-)).

Vou deixar o código falar; aqui está o que eu criei (usando testes do Visual Studio, mas deve funcionar usando o NUnit também):

DataServiceMockutiliza TaskCompletionSource<T>. Isso nos permite definir o resultado em um ponto definido na execução do teste que leva ao teste a seguir. Observe que estamos usando um representante para devolver o TaskCompletionSource de volta ao teste. Você também pode colocar isso no método Initialize do teste e usar propriedades.

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;

O que está acontecendo aqui é que primeiro verificamos que podemos deixar o método sem bloquear (isso não funcionaria se alguém fosse acessado Task.Result- nesse caso, teríamos um tempo limite porque o resultado da tarefa não seria disponibilizado até que o método retornasse )
Em seguida, definimos o resultado (agora o método pode ser executado) e verificamos o resultado (em um teste de unidade, podemos acessar o Task.Result, pois na verdade queremos que o bloqueio ocorra).

Classe de teste completa - BarTestobtém êxito e FooTestfalha conforme desejado.

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

E uma pequena turma auxiliar para testar deadlocks / timeouts:

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

Boa resposta. Estou planejando tentar seu código quando tiver algum tempo (na verdade não sei ao certo se funciona ou não), mas parabéns e um voto positivo pelo esforço.
Robert Harvey

-2

Aqui está uma estratégia que eu usei em um aplicativo enorme e muito, muito multithread:

Primeiro, você precisa de alguma estrutura de dados em torno de um mutex (infelizmente) e não cria um diretório de chamadas de sincronização. Nessa estrutura de dados, há um link para qualquer mutex bloqueado anteriormente. Todo mutex tem um "nível" começando em 0, que você atribui quando o mutex é criado e nunca pode mudar.

E a regra é: se um mutex estiver bloqueado, você só deve bloquear outros mutexes em um nível inferior. Se você seguir essa regra, não poderá ter conflitos. Quando você encontra uma violação, seu aplicativo ainda está funcionando perfeitamente.

Quando você encontra uma violação, há duas possibilidades: Você pode ter atribuído os níveis incorretamente. Você bloqueou A seguido pelo bloqueio B, então B deveria ter um nível mais baixo. Então você fixa o nível e tenta novamente.

A outra possibilidade: você não pode consertar. Algum código seu bloqueia A seguido pelo bloqueio B, enquanto outro código bloqueia B seguido pelo bloqueio A. Não há como atribuir os níveis para permitir isso. E é claro que esse é um impasse em potencial: se os dois códigos forem executados simultaneamente em threads diferentes, há uma chance de impasse.

Após a introdução, houve uma fase bastante curta em que os níveis tiveram que ser ajustados, seguida por uma fase mais longa em que foram encontrados possíveis impasses.


4
Sinto muito, como isso se aplica ao comportamento assíncrono / aguardar? Não posso injetar realisticamente uma estrutura de gerenciamento de mutex personalizada na Biblioteca Paralela de Tarefas.
Telastyn 4/11/2015

-3

Você está usando o Async / Await para poder paralelizar chamadas caras como em um banco de dados? Dependendo do caminho de execução no banco de dados, isso pode não ser possível.

A cobertura de teste com async / waitit pode ser desafiadora e não há nada como o uso real da produção para encontrar bugs. Um padrão que você pode considerar é passar um ID de correlação e registrá-lo na pilha e, em seguida, ter um tempo limite em cascata que registra o erro. Esse é mais um padrão SOA, mas pelo menos daria uma noção de onde ele vem. Usamos isso com o Splunk para encontrar impasses.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.