await работает, но вызывает задачу. Результат зависает / блокируется

У меня есть следующие четыре теста и последний зависает, когда я запускаю его, мой вопрос, почему это происходит:

[Test]
public void CheckOnceResultTest()
{
    Assert.IsTrue(CheckStatus().Result);
}

[Test]
public async void CheckOnceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceResultTest()
{
    Assert.IsTrue(CheckStatus().Result); // This hangs
    Assert.IsTrue(await CheckStatus());
}

private async Task<bool> CheckStatus()
{
    var restClient = new RestClient(@"https://api.test.nordnet.se/next/1");
    Task<IRestResponse<DummyServiceStatus>> restResponse = restClient.ExecuteTaskAsync<DummyServiceStatus>(new RestRequest(Method.GET));
    IRestResponse<DummyServiceStatus> response = await restResponse;
    return response.Data.SystemRunning;
}

Я использую этот метод расширения для Restsharp RestClient:

public static class RestClientExt
{
    public static Task<IRestResponse<T>> ExecuteTaskAsync<T>(this RestClient client, IRestRequest request) where T : new()
    {
        var tcs = new TaskCompletionSource<IRestResponse<T>>();
        RestRequestAsyncHandle asyncHandle = client.ExecuteAsync<T>(request, tcs.SetResult);
        return tcs.Task;
    }
}
public class DummyServiceStatus
{
    public string Message { get; set; }
    public bool ValidVersion { get; set; }
    public bool SystemRunning { get; set; }
    public bool SkipPhrase { get; set; }
    public long Timestamp { get; set; }
}

Почему последний тест зависает?

6 ответов

Решение

Вы сталкиваетесь со стандартной тупиковой ситуацией, которую я описал в своем блоге и в статье MSDN: async метод пытается запланировать свое продолжение на поток, который блокируется вызовом Result,

В этом случае ваш SynchronizationContext это тот, который используется NUnit для выполнения async void методы испытаний. Я бы попробовал использовать async Task методы тестирования вместо.

Получение значения с помощью асинхронного метода:

var result = Task.Run(() => asyncGetValue()).Result;

Синхронный вызов асинхронного метода

Task.Run( () => asyncMethod()).Wait();

Из-за использования Task.Run проблем тупика не возникнет.

Вы можете избежать добавления тупика ConfigureAwait(false) к этой строке:

IRestResponse<DummyServiceStatus> response = await restResponse;

=>

IRestResponse<DummyServiceStatus> response = await restResponse.ConfigureAwait(false);

Я описал эту ловушку в своем блоге Pitfalls of async/await

Вы блокируете пользовательский интерфейс с помощью свойства Task.Result. В документации MSDN они четко упомянули, что

"Свойство Result является блокирующим свойством. Если вы попытаетесь получить к нему доступ до завершения своей задачи, текущий активный поток блокируется до тех пор, пока задача не завершится и значение не станет доступным. В большинстве случаев вы должны получить доступ к значению с помощью Await. или ждите, вместо того, чтобы получить доступ к собственности напрямую."

Лучшее решение для этого сценария - удалить await и async из методов и использовать только Task, где вы возвращаете результат. Это не испортит вашу последовательность выполнения.

Дополнение к ответу @HermanSchoenfeld. К сожалению, приведенная ниже цитата не соответствует действительности:

Никаких проблем с взаимоблокировкой не возникнет из-за использования Task.Run.

      public String GetSqlConnString(RubrikkUser user, RubrikkDb db) 
{ 
    // deadlock if called from threadpool, 
    // works fine on UI thread, works fine from console main 
    return Task.Run(() => 
        GetSqlConnStringAsync(user, db)).Result; 
}

Выполнение заключено в Task.Run, это позволит запланировать задачу в пуле потоков и заблокировать вызывающий поток. Это нормально, если вызывающий поток не является потоком пула потоков. Если вызывающий поток находится из пула потоков, происходит следующая катастрофа: новая задача помещается в очередь до конца очереди, и поток пула потоков, который в конечном итоге выполняет задачу, блокируется до тех пор, пока задача не будет выполнена.

В коде библиотеки нет простого решения, поскольку вы не можете предположить, в каком контексте вызывается ваш код. Лучшее решение - вызывать асинхронный код только из асинхронного кода, блокируя API синхронизации из методов синхронизации, а не смешивать их.

Источник:

https://medium.com/rubrikkgroup/understanding-async-avoiding-deadlocks-e41f8f2c6f5d

Если вы не получаете никаких обратных вызовов или элемент управления не зависает, после вызова Асинхронной функции Сервис /API.

Вы должны настроить контекст так, чтобы он возвращал результат в том же вызываемом контексте. использование TestAsync().ConfigureAwait(continueOnCapturedContext: false);

Вы столкнетесь с этой проблемой только в веб-приложениях, но не в Static void main

Другие вопросы по тегам