'await' работает, но при вызове task.Result зависает / блокируется

126

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

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

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

Йохан Ларссон
источник
7
Вам следует избегать возврата void из асинхронных методов. Это только для обратной совместимости с существующими обработчиками событий, в основном в коде интерфейса. Если ваш асинхронный метод ничего не возвращает, он должен вернуть Task. У меня было множество проблем с MSTest и недействительными асинхронными тестами.
ghord
2
@ghord: MSTest вообще не поддерживает async voidметоды модульного тестирования; они просто не будут работать. Однако NUnit это делает. Тем не менее, я согласен с общим принципом предпочтения async Taskбольшему async void.
Стивен Клири
@StephenCleary Да, хотя это было разрешено в бета-версиях VS2012, что вызывало всевозможные проблемы.
ghord

Ответы:

88

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

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

Стивен Клири
источник
4
переход на async Task сработал, теперь мне нужно пару раз прочитать содержимое ваших ссылок, сэр.
Johan Larsson
@MarioLopez: Решение состоит в том, чтобы использовать « asyncполностью» (как отмечено в моей статье MSDN). Другими словами, как гласит заголовок моего сообщения в блоге, «не блокируйте асинхронный код».
Стивен Клири
1
@StephenCleary, что, если мне нужно вызвать асинхронный метод внутри конструктора? Конструкторы не могут быть асинхронными.
Raikol Amaro 03
1
@StephenCleary Почти во всех ваших ответах на SO и в статьях все, о чем я когда-либо видел, что вы говорите, - это замена Wait()на создание метода вызова async. Но мне кажется, что это подталкивает проблему вверх по течению. В какой-то момент чем-то нужно управлять синхронно. Что, если моя функция целенаправленно синхронна, потому что она управляет долго выполняющимися рабочими потоками Task.Run()? Как мне дождаться завершения этого теста без взаимоблокировки внутри моего теста NUnit?
void.pointer
1
@ void.pointer: At some point, something has to be managed synchronously.- совсем нет. Для приложений пользовательского интерфейса точка входа может быть async voidобработчиком событий. Для серверных приложений точкой входа может быть async Task<T>действие. Предпочтительно использовать asyncоба, чтобы избежать блокировки потоков. Вы можете сделать ваш тест NUnit синхронным или асинхронным; если async, сделайте это async Taskвместо async void. Если он синхронный, то не должно быть, SynchronizationContextпоэтому не должно быть тупика.
Стивен Клири
223

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

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

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

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

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

Герман Шенфельд
источник
15
-1 за поощрение использования методов async voidмодульного тестирования и удаление гарантий того же потока, предоставляемых классом SynchronizationContextиз тестируемой системы.
Стивен Клири
68
@StephenCleary: async void не обнадёживает. Он просто использует допустимую конструкцию C # для решения проблемы взаимоблокировки. Приведенный выше фрагмент является незаменимым и простым решением проблемы OP. Stackoverflow - это решение проблем, а не многословная самореклама.
Герман Шенфельд
81
@StephenCleary: в ваших статьях решение не сформулировано (по крайней мере, неясно), и даже если бы у вас было решение, вы бы использовали такие конструкции косвенно. Мое решение явно не использует контексты, и что? Дело в том, что мой работает, и это однострочный. Для решения проблемы не потребовались два сообщения в блоге и тысячи слов. ПРИМЕЧАНИЕ: Я даже не использую async void , поэтому я действительно не знаю, о чем вы говорите .. Вы видите "async void" где-нибудь в моем кратком и правильном ответе?
Герман Шёнфельд
15
@HermanSchoenfeld, если вы добавите почему к как , я считаю, что ваш ответ принесет большую пользу.
ironstone13
19
Я знаю, что это немного поздно, но вы должны использовать .GetAwaiter().GetResult()вместо этого, .Resultчтобы ничего Exceptionне было упаковано.
Камило Теревинто,
15

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

IRestResponse<DummyServiceStatus> response = await restResponse;

=>

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

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

Владимир
источник
9

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

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

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

Темный рыцарь
источник
3

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

использование TestAsync().ConfigureAwait(continueOnCapturedContext: false);

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

Маянк Пандит
источник
ConfigureAwaitпозволяет избежать взаимоблокировки в определенных сценариях, не работая в исходном контексте потока.
davidcarr 05