23

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

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

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


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

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

  • Асинхронный

  • Асинхронный (паттерн Throttling)

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

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

Вывод:

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

1 Answers1

12

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

Запускать 10000 одновременных запросов это, конечно, перебор. Но запускать, скажем, по полсотни-сотне одновременных запросов -- вполне нормально. Такой шаблон называется троттлингом -- throttling (или в автомобильных терминах -- дросселированием :D). Т.е. пропускаем весь объем заданий по-немногу. Такой подход позволяет несильно загружать канал при отправке запросов и машину при получении и разборе ответов. Примерный код может выглядеть так:

public async Task CheckServers()
{
    var servers = new List<string>(10000) { ... };
const int ConcurrencyLevel = 100;

// запускаем первые 100 запросов
var tasks = servers.Take(ConcurrencyLevel).Select(GetVersion).ToList();
int nextIndex = ConcurrencyLevel;

while (tasks.Count &gt; 0)
{
    // дожидаемся завершения любого запроса
    var completedTask = await Task.WhenAny(tasks);

    // удаляем его из списка
    tasks.Remove(completedTask);

    // добавляем новый запрос, если таковые остались
    if (nextIndex &lt; servers.Count)
    {
        tasks.Add(GetVersion(servers[nextIndex++]));
    }

    string rfbVersion = await completedTask;
    // работаем с версией
}

}

private async Task<string> GetVersion(string server) { // тут асинхронная реализация обращения к серверу по RFB и возвращение версии }

Важный вопрос состоит в правильной асинхронной реализации обращения к серверу по протоколу RFB. Если вы используете библиотеку -- она должна поддерживать асинхронность. Если вы реализуете эту функциональность сами (например, на сокетах) -- значит нужно пользоваться асинхронными функциями сокетов.

UPD

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

andreycha
  • 25,167
  • 4
  • 46
  • 82
  • Хм, тоесть получается выигрыша не будет? Ресурсов у тестового сервера - навалом. Но толку от 500-1000 одновременных асинхронные обращений к серверам я не вижу. Они и на 20% не загрузят сервер. А переделывать синхронную работу с RFB в асинхронную - пустая трата времени. – Alexis Mar 16 '16 at 22:12
  • 1
    @Alexis а вы попробуйте. И увидите, будет ли выигрыш -- и по нагрузке (сегодня дали хороший сервер, завтра плохой), и по времени выполнения. И нам потом расскажете. – andreycha Mar 16 '16 at 22:20
  • @andreycha: А почему 10К запросов — перебор? Где bottleneck? – VladD Mar 16 '16 at 22:23
  • @VladD я как раз ответ дополнил этим. Узко может быть в сетевых соединениях на отправке и на процессоре/памяти при приеме ответов всем скопом. Это все тестировать, конечно, надо, потому что я не знаю, сколько ресурсов эта штука кушает. Может там и по тысяче нормально пойдет. – andreycha Mar 16 '16 at 22:28
  • @andreycha: Ну, если есть тулза, которая эти самые 10К коннектов выдерживает, то async/await их точно так же выдержит по идее. Я бы попробовал без throttling'а, а потом с ним. И сравнил. Если уж ресурсов много, и надо все их использовать по максимуму... – VladD Mar 16 '16 at 22:31
  • 1
    @VladD разумно. Про троттлинг я писал еще по первой версии вопроса, где не было никаких деталей (ни кода, ни уточнений в комментариях). – andreycha Mar 16 '16 at 22:35
  • @andreycha, проверьте, пожалуйста, свою реализацию паттерна. Результаты тестирования с исходниками проектов добавил в вопрос. Хотелось бы понять - моя это ошибка либо ваша. – Alexis Mar 17 '16 at 16:01
  • @VladD, добавил тестовые проекты в вопрос. – Alexis Mar 17 '16 at 16:03
  • @Alexis а какой concurrency level выставляли? Возможно что в вашем случае запросы и ответы настолько легковесны, что при данном сочетании количества запросов и имеющихся ресурсов троттлинг не имеет смысла, как и предполагал VladD. – andreycha Mar 17 '16 at 16:49
  • @andreycha, пробовал разные варианты, начиная от 100 и заканчивая половиной списка (150 000). – Alexis Mar 17 '16 at 16:50
  • 2
    @Alexis понятно. Ну значит в вашем случае троттлинг действительно не нужен. Спасибо за опубликованные результаты. – andreycha Mar 17 '16 at 16:55
  • @Alexis возможно у вас остался код тестовых вариантов (в вопросе ссылок больше нет). Вопрос интересный, сам бы поковырял примеры... Вы не могли бы выложить код заново? Возможно выслать мне на почту? – nikita Jun 07 '16 at 13:35
  • 1
    @nikita, та ссылка умерла, вот новая: https://www.sendspace.com/file/mxw469 – Alexis Jun 07 '16 at 13:38
  • Что-то реализация throttle какая-то многословная... Можно же и короче сделать, аж двумя способами. – Pavel Mayorov Jul 25 '17 at 16:39
  • Вариант первый: асинхронный семафор. Все задачи запускаются одновременно, но каждая сначала ждет на семафоре. – Pavel Mayorov Jul 25 '17 at 16:40
  • Вариант второй: очередь и набор исполнителей. Исполнители стартуют одновременно и по-очереди обрабатывают последовательность заданий. – Pavel Mayorov Jul 25 '17 at 16:40