Как масштабировать приложение до 50000 одновременных задач - PullRequest
2 голосов
/ 03 августа 2020

Я работаю над проектом, который должен иметь возможность запускать (например) 50 000 задач одновременно. Каждая задача будет выполняться с определенной частотой (скажем, 5 минут) и будет либо пингом URL-адреса, либо запросом HTTP GET. Мой первоначальный план состоял в том, чтобы создать поток для каждой задачи. Я провел базовый c тест, чтобы проверить, возможно ли это с учетом доступных системных ресурсов. Я запустил следующий код как консольное приложение:

public class Program
{
    public static void Test1()
    {
        Thread.Sleep(1000000);
    }

    public static void Main(string[] args)
    {
        for(int i = 0; i < 50000; i++)
        {
            Thread t = new Thread(new ThreadStart(Test1));
            t.Start();
            Console.WriteLine(i);
        }
    }
}

К сожалению, хотя он запускался очень быстро, на отметке 2000 потоков производительность сильно снизилась. К 5000 я мог считать быстрее, чем программа могла создавать потоки. Из-за этого кажется, что достижение 50000 невозможно. Я на правильном пути или мне стоит попробовать что-нибудь еще? Спасибо

1 Ответ

6 голосов
/ 03 августа 2020

Многие люди думают, что вам нужно создать n потоков, если вы хотите обрабатывать n задачи параллельно. В большинстве случаев компьютер ожидает ввода-вывода, такого как сетевой трафик c, доступ к диску, передача памяти для вычислений GPU, аппаратное устройство для завершения операции и т. Д. c.

Учитывая это понимание, мы можем видеть, что жизнеспособным решением для параллельной обработки максимально возможного количества задач для данной аппаратной платформы является конвейерная работа: поместить работу в очередь и обработать ее, используя как можно больше потоков. Обычно это означает 1-2 потока на виртуальный процессор.

В C# мы можем выполнить sh это с помощью параллельной библиотеки задач (TPL):

    class Program
    {
        static Task RunAsync(int x)
        {
            return Task.Delay(10000);
        }

        static async Task Main(string[] args)
        {
            var tasks = Enumerable.Range(0, 50000).Select(x => RunAsync());

            Console.WriteLine("Waiting for tasks to complete...");

            await Task.WhenAll(tasks);

            Console.WriteLine("Done");
        }
    }

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

Дополнительные соображения

С большая верхняя граница (n = 50000), вам следует учитывать нехватку памяти, активность сборщика мусора и другие накладные расходы, связанные с задачами. Следует учитывать следующее:

  • Рассмотрите возможность использования ValueTask , чтобы минимизировать выделения, особенно для синхронных операций
  • Используйте ConfigureAwait (false) там, где это возможно, чтобы уменьшить переключение контекста
  • Используйте CancellationTokenSource и CancellationToken для ранней отмены запросов (например, тайм-аут)
  • Следуйте рекомендациям
    • Избегайте ожидания внутри al oop, где это возможно
    • Избегайте слишком частого запроса задач для завершения
    • Избегайте доступа Task . Результат до того, как задача будет выполнена завершено для предотвращения блокировки
    • Избегайте взаимоблокировок, используя примитивы синхронизации (мьютекс, семафор, сигнал условия, синхронизация и т. д. c) в зависимости от ситуации
    • Избегайте частого использования Task.Run для создания задач, чтобы избежать исчерпания пула потоков, доступного для планировщика задач по умолчанию (этот метод обычно зарезервирован для задач с привязкой к вычислениям)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...