async/await в парсере

366
25 декабря 2016, 18:45

Нужно спарсить информацию о пользователях сайта.

Есть класс, который представляет пользователя:

class User
{
    public int Id {get; private set;}
    public string Name {get; private set;}  
}

И есть асинхронный метод, который получает информацию о пользователе по ссылке и возвращает экземпляр User:

public async Task<User> GetUserByUrl(string url)
{
    string source = await HttpGetAsync(url).ConfigureAwait(false);
    int id = Regex.Match(source, @"...").Value;
    string name = Regex.Match(source, @"...").Value;
    User user = new User();
    user.Id = id;
    user.Name = name;
    return user;
}

Главный метод перебирает коллекцию ссылок на пользователей и вызывает метод GetUserByUrl:

public async Task<List<User>> Parser(List<string> links)
{
    var users = new List<User>();
    for(int i = 0; i<links.Count; i++)
    {
        var user = await GetUserById(links.ElementAt(i)).ConfigureAwait(false); 
        users.Add(user);
    }
    return users;
}

Все бы ничего, но скорость работы очень медленная. Похоже, что все работает синхронно.

Раньше у меня было сделано так, что метод GetUserByUrl находился в классе User и ничего не возвращал, а изменял его же свойства.

Главный метод выглядел так и парсилось все гораздо быстрее:

public async Task<List<User>> Parser(List<string> links)
{
    var _tasks = new List<Task>();
    for(int i = 0; i<links.Count; i++)
    {
        var user = new User();
        _tasks.Add(Task.Run(() => user.GetUserByUrl(links.ElementAt(i))).ContinueWith(task => users.Add(user)));
    }
    await Task.WhenAll(_tasks);
    return users;
}

Почему в первом случае метод отрабатывает медленно (синхронно?) и как его заставить работать быстрее?

Answer 1

Асинхронность и многопоточноть - это две совершенно разные вещи.

Асинхронность - это возможность не блокировать выполнение на время сетевых или дисковых вызовов. Она не ускоряет выполнение сама по себе.

async/await - это инструмент асинхронности. Он просто позволяет вам делать что-то еще во время ожидания ответа от сервера.

Ваш цикл работает (возможно) асинхронно, но однопоточно - он просто не ставит на выполнение вытягивание следующего пользователя до тех пор, пока не получит данные предыдущего. Проблема в том, что вы явно ждете данных пользователя.

var user = await GetUserById(links.ElementAt(i)).ConfigureAwait(false); 
users.Add(user);

Очевидно, что выполнение просто не дойдет до следующей итерации цикла до тех пор, пока вы не вытяните и не распарсите данные текущей итерации.

Мнопоточность - это использование нескольких одновременных потоков выполнения для обработки данных.

Task.Run - это запуск делегата на выполнение в отдельном потоке из пула. Весь код, который вы в него забрасываете, выполняется в отдельных потоках. И видно, что вы сначала забрасываете вытягивание всех пользователей, а уже потом начинаете ждать результата. Естественно, это работает быстрее. Если вам не принципиально использовать async/await - оставьте этот вариант.

Только исправьте в нем проблему с доступом к списку users из разных потоков - List<T> не потокобезопасен - иначе вы будете терять данные. Стоит ждать таски как есть, а в список складывать их .Result-ы уже после завершения.

Вы можете получить тот же или близкий результат на async/await, если не будете ждать выполнения, а точно так же сложите результаты вызовов GetUserById в список тасков, и в конце точно так же сделаете await Task.WhenAll(_tasks);

Answer 2

Итак, что у нас есть асинхронного, но почему то не быстрого:

public async Task<List<User>> Parser(List<string> links)
{
    var users = new List<User>();
    for(int i = 0; i<links.Count; i++)
    {
        var user = await GetUserById(links.ElementAt(i)).ConfigureAwait(false); 
        users.Add(user);
    }
    return users;
}

Как это легко заставить шевелиться пошустрее:

public async Task<List<User>> Parser(List<string> links)
{
  var users = links.Select(GetUserById).ToList();
  await Task.WhenAll(users);
  return users.Select(u => u.Result).ToList();
}

Главное - не ждать конкретный элемент, чтобы его засунуть в результирующий список, а дождаться сразу всех и вернуть их.

Стоит понимать, что следующая после await строчка кода, работающая с результатом задачи будет этой самой задачи дожидаться. Поэтому в цикле их лучше не использовать, а строить коллекцию таких задачек и ждать их через WhenAll \ WhenAny.

Answer 3

В первом методе вы вызываете задачу и тут же ожидаете результата ее выполнения, и так для каждого элемента в List.

var user = await GetUserById(links.ElementAt(i)).ConfigureAwait(false); 

Во втором методе вы cначала запускаете все задачи (для каждого элемента)

_tasks.Add(Task.Run(() => user.GetUserByUrl(links.ElementAt(i))).ContinueWith(task => users.Add(user)));

и только потом ожидаете их.

await Task.WhenAll(_tasks);

Поэтому второй вариант и может быть быстрее.

READ ALSO
Подключение геймпада к UWP проекту

Подключение геймпада к UWP проекту

Есть UWP проект, к нему нужно подключить поддержку управления геймпадом(контроллер XBOX), как это можно реализовать? Есть ли какие-нибудь библиотеки...

367
Вложенные анонимные типы?

Вложенные анонимные типы?

Всем привет, имеется вот такой пример:

365
xPath selectSingleNode не могу выбрать элемент

xPath selectSingleNode не могу выбрать элемент

Добрый деньесть XML файл

537