Страницы

Поиск по вопросам

вторник, 26 ноября 2019 г.

Многопоточное vs асинхронное сетевое программирование на практике


Как-то в комментариях VladD поделился информацией, что один из его коллег, сетево
программист, перешел от многопоточного к асинхронному сетевому программированию. Хотелось бы на примере конкретной задачи разобраться, насколько асинхронность выиграет у многопоточности.

Задача: возьмем один из простых сетевых протоколов - RFB. Нам нужно одновременн
подключиться к 10 000 серверов с RFB на борту, и узнать версию RFB.

Как это реализовать многопоточно - я знаю, но как это реализовать асинхронно? И н
сколько, в данной задаче, асинхронность выиграет? Сама реализация RFB - не нужна, нужен пример выполнения 10 000 одновременных асинхронных запросов.



Протестировал 3 варианта кода:


Многопоточный (поменял разбивку списка на несколько - на потокобезопасную очередь, чтобы уровнять шансы)
Асинхронный
Асинхронный (паттерн Throttling)


Результаты (проверка 300 000 IP адресов):


Многопоточный: 3 минуты 18 секунд
Асинхронный: 1 минута 27 секунд
Асинхронный (паттерн Throttling): когда перевалило за 6 минут - закрыл программ
и не стал измерять дальше. Скорости можно добиться только использовав в уровне параллелизм
- размер всего списка, но тогда теряется смысл самого использования паттерна. Т.е. реализация от andreycha, если использовать уровень параллелизма меньше размера списка - работает дольше чем даже многопоточная версия. Возможно это просто моя ошибка, либо ошибка andreycha. 


Вывод:


Стандартная асинхронная реализация работает более чем в 2 раза быстрее чем многопоточная.

    


Ответы

Ответ 1



Про асинхронность и ее преимущества тут. Вкратце -- в то время, пока запрос уше в сеть и не вернулся обратно, мы не блокируем потоки на нашем компьютере. Т.о. 10000 адресов можно вполне обработать, например, несколькими потоками. Запускать 10000 одновременных запросов это, конечно, перебор. Но запускать, скажем по полсотни-сотне одновременных запросов -- вполне нормально. Такой шаблон называетс троттлингом -- throttling (или в автомобильных терминах -- дросселированием :D). Т.е. пропускаем весь объем заданий по-немногу. Такой подход позволяет несильно загружать канал при отправке запросов и машину при получении и разборе ответов. Примерный код может выглядеть так: public async Task CheckServers() { var servers = new List(10000) { ... }; 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 GetVersion(string server) { // тут асинхронная реализация обращения к серверу по RFB и возвращение версии } Важный вопрос состоит в правильной асинхронной реализации обращения к серверу п протоколу RFB. Если вы используете библиотеку -- она должна поддерживать асинхронность. Если вы реализуете эту функциональность сами (например, на сокетах) -- значит нужно пользоваться асинхронными функциями сокетов. UPD Выяснилось, что запросы/ответы у ТС настолько легковесные, что в данном случае троттлин работает медленнее, чем если отправить сразу все запросы. Однако этот паттерн може быть по-прежнему полезен, когда необходимо ограничить количество исходящих запросов и/или количество обрабатываемых ответов (например, если разбор ответов сильно загружает процессор/требует много памяти).

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

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