среда, 14 ноября 2012 г.

Как заставить async метод вести себя как синхронный

Недавно, я в гневе писал вот эту статью, про то, что методы помеченные как async ведут себя не так, как от них ожидается. Давайте, сегодня я покажу небольшой пример на то, как вернуть им возможность выполниться как синхронным.
Итак, давайте возьмем простенький пример с загрузкой изображений. Для этого, я накидал небольшую формочку вот с такой разметкой:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto" />
        <RowDefinition Height="1*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <TextBox Margin="5,5,100,5" x:Name="tbURI" Grid.ColumnSpan="2" />
    <Button HorizontalAlignment="Right" Margin="5" Width="90" Content="Скачать" Grid.ColumnSpan="2" Click="Button_Click" />
    <TextBox Margin="5" IsReadOnly="True" x:Name="tbLog" Grid.Row="1" TextWrapping="Wrap" />
    <Image Margin="5" x:Name="imView" Grid.Row="1" Grid.Column="1" Stretch="UniformToFill" />
</Grid>
 
Ну и под все это вот такой код:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    void WriteLog(string p_message)
    {
        Dispatcher.Invoke((Action)(
            () => tbLog.Text += string.Format("{0:hh:mm:ss} - {1}\n", DateTime.Now, p_message)
            ));
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        WriteLog("Начало скачивания");
        DownloadImage();
        WriteLog("Окончание скачивания");
    }

    private void DownloadImage()
    {
        byte[] buffer = (new WebClient()).DownloadData(new Uri(tbURI.Text));
        WriteLog("Изображение считано");
        BitmapImage source = new BitmapImage();
        source.BeginInit();
        source.StreamSource = new MemoryStream(buffer);
        source.EndInit();
        imView.Source = source;
        WriteLog("Изображение отображено");
    }
}

Все достаточно просто. Вводим URL, тыкаем кнопку и видим картинку. Плюс, у нас есть возможность смотреть порядок выполнения действий выполняемых программой:
 Пока все в последовательности логично. Единственно, что раздражает в нашей программе, это "зависание" при скачивании большой картинки. Обратите внимание на время между началом работы обработчика кнопки и окончанием.
К счастью, о нас уже подумали и дали нам возможность воспользоваться вместо метода DownloadData его асинхронным собратом DownloadDataTaskAsync. Правим код:

private async void DownloadImage()
{
    byte[] buffer = await (new WebClient()).DownloadDataTaskAsync(new Uri(tbURI.Text));
    WriteLog("Изображение считано");
    BitmapImage source = new BitmapImage();
    source.BeginInit();
    source.StreamSource = new MemoryStream(buffer);
    source.EndInit();
    imView.Source = source;
    WriteLog("Изображение отображено");
}

Не привожу метод обработчик клика на кнопке, т.к. он не поменялся, т.е. в нем вызов DownloadImage по прежнему выглядит как синхронный.
Запускаем:
Приложение больше не "зависает", но у него изменился порядок вывода. Теперь, приложение считает, что скачивание завершилось мгновенно. И если у на вместо вывода в лог этого радостного события будет  обработка загруженного изображения... Ну вы поняли. Ничего хорошего не будет.
Как с этим бороться? С одной стороны достаточно легко, с другой, мы получив проблему на одном уровне, просто транслируем ее на уровень выше (в данном случае это нам ничем не грозит, а вот в других случаях, возможно описанное решение придется применять и дальше).
Теперь придется править не только код метода отвечающего за скачивание, но и метод его вызывающий:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    WriteLog("Начало скачивания");
    await DownloadImageAsync();
    WriteLog("Окончание скачивания");
}

private async Task DownloadImageAsync()
{
    byte[] buffer = await (new WebClient()).DownloadDataTaskAsync(new Uri(tbURI.Text));
    WriteLog("Изображение считано");
    BitmapImage source = new BitmapImage();
    source.BeginInit();
    source.StreamSource = new MemoryStream(buffer);
    source.EndInit();
    imView.Source = source;
    WriteLog("Изображение отображено");
}

Как видно, изменений три:
1. Изменен тип возвращаемого значения у скачивающего картинку метода с void на Task.
2. К имени этого метода добавлен суффикс Async (рекомендуют так делать, чтобы эти методы были легко узнаваемыми).
3. Обработчик клика на кнопке сам стал async.
Все, теперь все работает как и ожидалось:
На этом можно было бы и закончить рассказ, если бы не одно но, про которое должен был возникнуть вопрос при взгляде на метод DownloadImageAsync. У него изменился тип возвращаемого значения с void на Task, а вот строчки return, у него как не было, так и нет. Это еще один "синтаксический сахар", который появился в c# 5. Ну а теперь, действительно все. 

Комментариев нет:

Отправить комментарий