Нужно спарсить информацию о пользователях сайта.
Есть класс, который представляет пользователя:
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;
}
Почему в первом случае метод отрабатывает медленно (синхронно?) и как его заставить работать быстрее?
Асинхронность и многопоточноть - это две совершенно разные вещи.
Асинхронность - это возможность не блокировать выполнение на время сетевых или дисковых вызовов. Она не ускоряет выполнение сама по себе.
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);
Итак, что у нас есть асинхронного, но почему то не быстрого:
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.
В первом методе вы вызываете задачу и тут же ожидаете результата ее выполнения, и так для каждого элемента в 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);
Поэтому второй вариант и может быть быстрее.
Сборка персонального компьютера от Artline: умный выбор для современных пользователей