'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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.