Проблемы использования 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);
    }
}

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