Задержка пула потоков очень высока на однопроцессорной машине?

Я использую пул потоков для параллельного выполнения задачи.

вот код, который я использую:

pragma once
#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

class ThreadPool {
public:
  ThreadPool(size_t threads) : stop(false) {
    for(size_t i = 0; i < threads; ++i)
      workers.emplace_back(  // thread variable, emplace back just pass in a function for thread to construct
        [this] {
          for(;;) {
            std::function<void()> task;
            {   
              std::unique_lock<std::mutex> lock(this->queue_mutex);
              this->condition.wait(lock, [this] { return this->stop || !this->tasks.empty(); }); 
              if (this->stop && this->tasks.empty()) return;  // only on stop and empty task, perfect exit
              task = std::move(this->tasks.front());  // get the first task
              this->tasks.pop();
            }   
            task();
          }   
        }); 
  }


  template<class F, class... Args>
  auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {
    using return_type = typename std::result_of<F(Args...)>::type;
    auto task = std::make_shared< std::packaged_task<return_type()> >(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
    std::future<return_type> res = task->get_future();
    {   
      std::unique_lock<std::mutex> lock(queue_mutex);
      // don't allow enqueueing after stopping the pool
      if (stop) throw std::runtime_error("enqueue on stopped ThreadPool");
      tasks.emplace([task](){ (*task)(); }); 
    }   
    condition.notify_one();
    return res;
  }

  ~ThreadPool() {
    {   
      std::unique_lock<std::mutex> lock(queue_mutex);
      stop = true;
    }   
    condition.notify_all();
    for(std::thread &worker: workers) worker.join();
  }
private:
    std::vector< std::thread > workers;
    std::queue< std::function<void()> > tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};


main.cpp это:

#include "./thread_pool.hpp"
#include <sys/time.h>

int main() {
  ThreadPool tp(16);
  timeval t;
  for (size_t i = 0; i < 16; ++i) {
    gettimeofday(&t, NULL);
    tp.enqueue([t, i]() {
      timeval t1; 
      gettimeofday(&t1, NULL);
      int lat = (t1.tv_sec - t.tv_sec) * 1000000 + t1.tv_usec - t.tv_usec;
      printf("this is loop %zu, lat = %d\n", i, lat);
    }); 
  }
}

я тестирую задержку этого кода (задержку накладных расходов).

результат меня удивил.

на моей машине i9 10900k с 16 процессорами задержка составляет всего около 30 мкс для каждой задачи.

на моей машине aws с двумя процессорами задержка достигнет > 10000 мкс.

Я знаю, что 2-процессорный процессор с 16 потоками - это не настоящая паралла, но я думаю, что он будет замедляться только с 30-50 мкс.

до 10 000 долларов США — это намного больше, чем я ожидал, где же временные затраты?

иметь такое же количество рабочих потоков, как и количество имеющихся у вас виртуальных ядер.

Ahmed AEK 22.04.2024 15:36

спасибо, я попытался уменьшить размер потока, задержка все еще высокая (> 1 мс), просто хочу знать, почему

kevin h 22.04.2024 15:39

Отсутствие владения машиной определенно способствует этому, это работает с виртуализацией на машине, где выполняются другие рабочие нагрузки, ОС может потребоваться некоторое время, чтобы выделить для вас ядро.

Ahmed AEK 22.04.2024 15:48

@AhmedAEK, значит, сервер aws занят выполнением задачи, он задерживает меня на 10 мс, чтобы выделить процессор для выполнения задачи?

kevin h 22.04.2024 15:52

Если кто-то из сотрудников AWS не подтвердит, что мы можем только предполагать, вам, вероятно, следует вместо этого проверять пропускную способность или усреднять задержку по большому количеству запросов, принимая во внимание эти случайные всплески, для этого есть специальные тесты (p99), ваш простой код на самом деле не дает никакой полезной информации

Ahmed AEK 22.04.2024 15:55

вы используете одну и ту же ОС на каждой машине?

Alan Birtles 22.04.2024 16:12
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
68
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

на моей машине i9 10900k с 16 процессорами задержка составляет всего около 30 мкс для каждой задачи.

Если ядра доступны, то готовый поток может запуститься напрямую, следовательно, низкая задержка.

Я знаю, что 2-процессорный процессор с 16 потоками - это не настоящая паралла, но я думаю, что он будет замедляться только с 30-50 мкс.

В этом случае квант планирования является источником гораздо более высокой задержки. Оно не может быть слишком маленьким, потому что переключение контекста на обычных машинах обходится дорого (обычно порядка микросекунд, а то и десятков микросекунд из-за совсем недавних обновлений безопасности). В результате получить такт длительностью 30–50 мкс практически невозможно без огромных накладных расходов. Обратите внимание, что тактовое время зависит от целевой платформы (в основном от ОС и архитектуры).

Если вам нужны такие низкие накладные расходы, вы можете использовать программные пользовательские потоки и определяемое пользователем совместное планирование. Если вас волнует задержка, то количество готовых потоков никогда не должно превышать количество ядер. Настройка задержки целевого пула потоков также может помочь уменьшить задержку более важных операций.

Другие вопросы по теме