вторник, 16 октября 2012 г.

Про непонимание новомодных штук и к чему это приводит

На работе, уже два дня бегаю по руинам. Почему? А потому, что одному коллеге показалось, что новомодные штуки это круто, а раз круто, то должно быть и у нас. Вот про то, как работающий код был убит новомодными async и await, я и расскажу под катом.

 Чтобы не приводить огромные куски реального кода, буду объяснять на пальцах. Собственно, при использовании OData сервисов для доступа к базе данных, все весьма похоже на работу с БД через EntityFramework. Соответственно в наличии был синхронный метод Where. Т.к. объем передаваемых данных достаточно велик, синхронное выполнение замедляло работу. Предопределенного асинхронного метода не было, поэтому был написан свой. Для простоты, пусть он будет иметь вот такой вид:

static class Test
{
    public static IEnumerable<T> SlowWhere(this List<T> source, Func<T, bool> predicate)
    {
        Thread.Sleep(1000);
        IEnumerable result = source.Where(predicate).ToList();
        return result;
    }
}

В данном методе, есть все для демонстрации работы, включая длительную задержку. Давайте убедимся, что все работает. Например, можно воспользоваться приложением с одной кнопкой и текстблоком с именем tbResult вот так:


private void btTest_Click(object sender, RoutedEventArgs e)

{
    List<int> ints = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
    foreach (var item in ints.SlowWhere(number => number % 2 == 0))
    {
        tbResult.Text += string.Format("{0}\n", item);
    }
}
Все работает как задумывалось. Приложение запускается, по нажатию на кнопку - секунду тупит, а затем, выдает на экран четные числа. Ок, делаем асинхронный вызов синхронного метода.
Первый вариант, который и был реализован, выглядел примерно так:

public static void WhereA(this List<T> source, Func<T, bool> predicate, Action<IEnumerable<T>> loadedHandler)
{
    var thread = new Thread(
        () =>
        {
            // Медденная операция в отдельном потоке
            var result = source.SlowWhere(predicate);
            // Вызов метода по завершении длительной операции
            Application.Current.Dispatcher.BeginInvoke((Action)(
                () => loadedHandler(result)),
                null);
        }
        );
    thread.Start();
}

Переделываем под него обработчик клика по кнопке:

private void btTest_Click(object sender, RoutedEventArgs e)
{
    List<int> ints = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
    ints.WhereA(
        i => i % 2 == 0,
        result =>
        {
            foreach (var item in result)
            {
                tbResult.Text += string.Format("{0}\n", item);
            }
        }
        );
}

Запускаем, и хотя числа появляются через секунду, во время этой секунды приложение можно таскать по экрану (добавить мотылятор или что там надо делать во время ожидания).
Но, у данного решения есть два недостатка. Первый, заключается в том, что WhereA, ведет себя не так, как остальные расширяющие методы Where из Linq (не возвращает коллекцию). Второй недостаток, заключается в том, что надо либо писать дополнительный метод, который обрабатывает возвращенную коллекцию, либо писать сложные лямбда выражения, как в приведенном выше примере.
Ок, надо улучшать. А тут как раз есть async и await, ну как ими не воспользоваться? Добавляем еще один метод, который будет выполнять загрузку данных асинхронно:

public static Task<IEnumerable<T>> WhereAsync(this List<T> source, Func<T, bool> predicate)
{
    var task = new Task<IEnumerable>(
        () =>
        {
            var result = source.SlowWhere(predicate);
            return result;
        }
        );
    task.Start();
    return task;
}

Метод, по сложности аналогичен предыдущему, может даже чуть проще, т.к. не надо переключать контекст, но вот вызов, значительно проще:
 
private async void btTest_Click(object sender, RoutedEventArgs e)
{
    List<int> ints = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
    var result = await ints.WhereAsync(number => number % 2 == 0);
    foreach (var item in result)
    {
        tbResult.Text += string.Format("{0}\n", item);
    }
}

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

private void btTestWithTimer_Click(object sender, RoutedEventArgs e)
{
    tbResult.Text += string.Format("{0:hh:mm:ss}\n", DateTime.Now);
    btTest_Click(sender, e);
    tbResult.Text += string.Format("{0:hh:mm:ss}\n", DateTime.Now);
}

Согласитесь, все логично? Выводим время, вызываем обычный метод (не указав никаких дополнительных модификаторов), опять выводим время. Все хорошо? А вот и нет. Посмотрите на картинку:
Обратили внимание, на то, что сначала выведено два раза время (потом, кстати, прошла секунда), а потом только числа? Вот так и работают методы помеченные async, за два раза. До await, разрыв и продолжают работать. Я об этом уже писал. Больше всего, меня расстраивает то, что во втором методе о таком поведении, якобы синхронного метода, ничего не говорит. И вот это расстраивает...

1 комментарий:

  1. Для асинхронной, а как и впрочем для синхронной работы с OData используется специальный класс DataServiceQuery. У этого класса есть замечательный метод BeginExecute ( сигнатура метода: IAsyncResult BeginExecute(AsyncCallback callback, object state) ), который выполняет запрос инкапсулированный в экземпляр DataServiceQuery асинхронно. Соответственно после выполнения операции вызывается метод из делегата callback. При этом может выполняться одновременно несколько запросов к сервису (поверял лично). Ну и к вопросу о нововведениях….имхо придумывать асинхронную загрузку там где она есть что называется из «коробки», наверное, не стоит, хотя Task, аsync и await безусловно очень интересны.

    ОтветитьУдалить