Andrey on .NET | Проблемы использования async void методов

Проблемы использования async void методов

Создавать и использовать async void методы необходимо с большой осторожностью. Все дело в том, что они способны вызвать непредсказуемое поведение приложения. Поэтому использовать их можно только в случае, если разработчик твердо уверен в том, что он делает.

Давайте посмотрим на причины сделанного выше утверждения.

Невозможно использовать await (т.е. ожидать результат)

Предположим в классе есть следующий метод, загружающий текст страницы с сайта в поле _content:

private string _content;

private async void GetPageAsync(string url)
{
    var client = new HttpClient();
    this._content = await client.GetStringAsync(url);
}

На первый взгляд может показаться что все в порядке. Но использовать GetPageAsync() совместно с await не получится. Поэтому вызывающий код будет примерно следующего вида:

public void PrintLength()
{
    this.GetPageAsync("http://ya.ru");
    Console.WriteLine("Content length: {0}", this._content.Length);
}

В этом случае, как только выполнение дойдет до await client.GetStringAsync(), в основном потоке тут же продолжится выполнение PrintLength(). Соответственно, Console.WriteLine() выкинет исключение, т.к. в этот момент client.GetStringAsync() еще не загрузит страницу и _content будет равен null.

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

И, наконец, рассмотрим случай, если между вызовом GetPageAsync() и обращением к полю _content будет совершаться какая-то работа. Для примера имитируем её вызовом Thread.Sleep():

public void PrintLenght()
{
    this.GetPageAsync("http://ya.ru");
    Thread.Sleep(new Random().Next(5) * 1000); // "спим" от 0 до 5 секунд
    Console.WriteLine("Content length: {0}", this._content.Length);
}

Теперь поведение кода будет полностью зависеть от продолжительности "сна". Если она меньше, чем время загрузки страницы, то получим исключение или старое значение _content, если же больше – WriteLine() отобразит на консоли корректный результат.

Невозможно определить когда выполнение завершится

У вызывающего кода нет стандартных способов определить состояние запущенного async void метода. Как правило их вызов может быть описан фразой "запустил и забыл".

Вызывающий код не может перехватить возникающие исключения

Исключение, выброшенное в async void методе, не будет передано в вызывающий поток. А это значит, что отреагировать на него вызывающий метод не сможет. Например, даже не смотря на отсутствие каких-либо вызовов в GetPageAsync(), исключение не будет перехвачено блоком try-catch.

private async void GetPageAsync(string url)
{
    throw new Exception();
}

public void PrintLenght()
{
    try {
        this.GetPageAsync("http://ya.ru");
        Console.WriteLine("Content length: {0}", this._content.Length);
    }
    catch (Exception ex) {
        Console.WriteLine(ex.Message);
    }
}

Подстановка async void метода в качестве делегата

Учитывая сказанное ранее, можно отметить, что подстановка async void метода в качестве делегата может привести к непредсказуемым последствиям:

  • вызывающий код продолжит выполнение сразу после первого встреченного внутри делегата await;
  • перехватить выброшенное исключение не удастся.

Так что же делать с async void?

Ответ очень простой: заменить на async Task. Это позволит вызывающему коду использовать await и контролировать состояние через экземпляр объекта Task. Кроме того, механизм async/await обеспечит возможность обработки выброшенных исключений.

Приведенный ранее в качестве примера код можно переписать так:

private string _content;

private async Task GetPageAsync(string url)
{
    var client = new HttpClient();
    this._content = await client.GetStringAsync(url);
}

public async void PrintLenght()
{
    try {
        await this.GetPageAsync("http://ya.ru");
        Console.WriteLine("Content length: {0}", this._content.Length);
    }
    catch (Exception ex) {
        Console.WriteLine(ex.Message);
    }
}

Комментарии (5) -

Прошу прощения, но применение async void в даном случае было не правильно. Это такой эе инструмент как и async Task но он служит своим целям.

async void === Task.Run(SomeFunction)

То есть это нужно именно для того чтобы запустить таск без ожидания его завершения в более простой форме, но нужно не забыть сделать обработку ошибок внутри метода конечно, вот живой пример:

var listener = new HttpListener();
while (listener.IsListening)
{
   ProcessRequest( _listener.GetContext());
}

private async void ProcessRequest(HttpListenerContext context)
{
            try
            {
            }
            catch (Exception err)
            {
                LogError(err);
            }
}

Конечно всегда нужно понимать чего вы хотите добиться.

Evgeny Хорошо, в чем вы видите преимущество написания "async void" вместо "async Task". Т.е. вы сознательно создаете ограничения для вызывающей стороны. Возможные минусы описаны выше. А в чем возможные плюсы?

Алкусандр 20.06.2019 11:38:00

Есть репозиторий, в который при запуске программы начинает загружаться некий контент:

public static class PositionRepository
{
public static List<V_POSITION_LIST> List;

public static async Task<int> LoadData()
{
using (var client = new HttpClient())
{
response = await client.GetAsync(ProjectProperties.ApiRoutesV_POSITION_LIST);
response.EnsureSuccessStatusCode();
List= await response.Content.ReadAsAsync<List<V_POSITION_LIST>>();
}
}

public static List<V_POSITION_LIST> GetAll(long ContractType)
{
return List.Where(w => w.AGREEMENT_TYPE_ID == ContractType).ToList();
}
}

Запуск процедуры в начале программы осуществляется просто:
PositionRepository.LoadData();

В классике, если этих репозиториев много, то это оформляется в Таски и выполняется:
Task.WaitAll(T1, T2);

Но мне этого не надо, так как клиент может открыть, а может нет форму, где используется этот репозиторий.
Но в форме я должен обратиться к GetAll(), но если List не загружен до конца, то выдается ошибка.

Вопрос:
Как можно изменить загрузку и выгрузку, чтобы в GetAll() было понятно загружен List или нет (Если загружен, то выгружаем, если нет, то ждем)?

Можно проверять, завершилась ли таска загрузки. Если нет - просить клиента подождать.

Некропостинг.

Evgeny
>>> async void === Task.Run(SomeFunction)
Нет, это не так.

Andrey
>>в чем вы видите преимущество написания "async void" вместо "async Task"
Его нету, но оно бывает полезно для того, чтобы удобно стартовать таки к примеру в WinForms приложениях, где обработчики создаются как void методы. Или при наследовании от интерфейса из сторонней библиотеки, на который не можете повлиять и поменять на Task по причине обратной совместимости. И то в таких случаях часто будет выгоднее идти через Task.Run, но в зависимости от контекста синхронизации или отсутствия оного можно попасть на thread starvation, или переблочить все нахер

Добавить комментарий