Как-то в комментариях VladD поделился информацией, что один из его коллег, сетевой программист, перешел от многопоточного к асинхронному сетевому программированию. Хотелось бы на примере конкретной задачи разобраться, насколько асинхронность выиграет у многопоточности.
Задача: возьмем один из простых сетевых протоколов - RFB. Нам нужно одновременно подключиться к 10 000 серверов с RFB на борту, и узнать версию RFB
Как это реализовать многопоточно - я знаю, но как это реализовать асинхронно? И на сколько, в данной задаче, асинхронность выиграет? Сама реализация RFB - не нужна, нужен пример выполнения 10 000 одновременных асинхронных запросов.
Протестировал 3 варианта кода:
Многопоточный (поменял разбивку списка на несколько - на потокобезопасную очередь, чтобы уровнять шансы)
Асинхронный
Асинхронный (паттерн Throttling)
Результаты (проверка 300 000 IP адресов):
Многопоточный: 3 минуты 18 секунд
Асинхронный: 1 минута 27 секунд
Асинхронный (паттерн Throttling): когда перевалило за 6 минут - закрыл программу и не стал измерять дальше. Скорости можно добиться только использовав в уровне параллелизма - размер всего списка, но тогда теряется смысл самого использования паттерна. Т.е. реализация от andreycha, если использовать уровень параллелизма меньше размера списка - работает дольше чем даже многопоточная версия. Возможно это просто моя ошибка, либо ошибка andreycha.
Вывод:
Стандартная асинхронная реализация работает более чем в 2 раза быстрее чем многопоточная.
Ответ
Про асинхронность и ее преимущества тут. Вкратце -- в то время, пока запрос ушел в сеть и не вернулся обратно, мы не блокируем потоки на нашем компьютере. Т.о. 10000 адресов можно вполне обработать, например, несколькими потоками.
Запускать 10000 одновременных запросов это, конечно, перебор. Но запускать, скажем, по полсотни-сотне одновременных запросов -- вполне нормально. Такой шаблон называется троттлингом -- throttling (или в автомобильных терминах -- дросселированием :D). Т.е. пропускаем весь объем заданий по-немногу. Такой подход позволяет несильно загружать канал при отправке запросов и машину при получении и разборе ответов. Примерный код может выглядеть так:
public async Task CheckServers()
{
var servers = new List
const int ConcurrencyLevel = 100;
// запускаем первые 100 запросов
var tasks = servers.Take(ConcurrencyLevel).Select(GetVersion).ToList();
int nextIndex = ConcurrencyLevel;
while (tasks.Count > 0)
{
// дожидаемся завершения любого запроса
var completedTask = await Task.WhenAny(tasks);
// удаляем его из списка
tasks.Remove(completedTask);
// добавляем новый запрос, если таковые остались
if (nextIndex < servers.Count)
{
tasks.Add(GetVersion(servers[nextIndex++]));
}
string rfbVersion = await completedTask;
// работаем с версией
}
}
private async Task
Важный вопрос состоит в правильной асинхронной реализации обращения к серверу по протоколу RFB. Если вы используете библиотеку -- она должна поддерживать асинхронность. Если вы реализуете эту функциональность сами (например, на сокетах) -- значит нужно пользоваться асинхронными функциями сокетов.
UPD
Выяснилось, что запросы/ответы у ТС настолько легковесные, что в данном случае троттлинг работает медленнее, чем если отправить сразу все запросы. Однако этот паттерн может быть по-прежнему полезен, когда необходимо ограничить количество исходящих запросов и/или количество обрабатываемых ответов (например, если разбор ответов сильно загружает процессор/требует много памяти).
Комментариев нет:
Отправить комментарий