Для некоторой части моего проекта мне нужна система локального планирования процессов, которая позволит мне отложить выполнение метода на несколько секунд. У меня есть тысячи «клиентов» этой системы, поэтому использование threading.Timer для каждой задержки - плохая идея, потому что я быстро достигну предела потока ОС. Я внедрил систему, в которой для контроля времени используется только один поток.

Основная идея состоит в том, чтобы сохранить отсортированную задачу (time + func + args + kwargs) и использовать один threading.Timer для планирования / отмены выполнения заголовка этой очереди. Эта схема работает, но я не доволен производительностью. ~ 2000 клиентов, которые планируют фиктивные задачи каждые ~ 10 секунд, заставляют процесс занимать 40% процессорного времени. Глядя на вывод профилировщика, я вижу, что все время затрачивается на создание нового threading.Timer, его запуск и особенно на создание новых потоков.

Я считаю, что есть лучший способ. Теперь я подумываю переписать LightTimer, чтобы был один поток выполнения, управляемый threading.Event, и несколько потоков времени, которые будут set() события. Например:

  • Я планирую задачу для вызова в 10 секунд. Задача добавлена в очередь. Синхронизация # 1 начинается time.sleep(10) до event.set()
  • Затем я планирую задачу для вызова в 11 секунд. Задача добавлена в очередь. Ничего не происходит с потоком синхронизации, он обнаружит новую задачу после пробуждения.
  • Затем я планирую задачу для вызова через 5 секунд. Задача добавляется в очередь. Синхронизация # 2 запускается time.sleep(5), потому что # 1 уже спит в течение более длительного интервала.

Я надеюсь, что вы поймали эту идею. Что вы думаете об этом пути? Есть ли способ лучше? Может быть, я могу использовать некоторые функции системы Linux, чтобы сделать оптимальное решение?

3
nkrkv 7 Июн 2010 в 17:56

3 ответа

Лучший ответ

Альтернативная реализация, которую вы можете использовать, состоит в том, чтобы использовать метод time.time() для вычисления абсолютного времени выполнения каждой функции в очереди. Поместите это время и вашу функцию для вызова в обертку объекта, которая переопределяет оператор сравнения, используя время выполнения для определения порядка. Затем используйте модуль heapq для поддержания минимальной кучи. Это обеспечит вам эффективную структуру данных, в которой элемент 0 кучи всегда является вашим следующим событием.

Одним из способов реализации реальных вызовов было бы использование отдельного потока для выполнения обратных вызовов. Куча должна быть защищена мьютексом, и вы можете использовать условную переменную для реализации планирования. В бесконечном цикле просто выполните поиск в следующий раз, чтобы выполнить функцию (элемент 0 кучи) и использовать метод wait() условной переменной с временем ожидания, установленным на следующее время выполнения. Ваш метод вставки в кучу может затем использовать метод условной переменной notify() для раннего пробуждения потока планирования, если только что вставленная функция должна появиться до самой ранней, уже существующей в куче.

2
Rakis 7 Июн 2010 в 15:12

Вы смотрели на модуль sched в Python стандартная библиотека? Запуск планировщика в выделенном потоке (и наличие всех запланированных действий: «поместите связанный метод и его аргументы в очередь», из которого потоки в пуле очищают и выполняют его - так же, как я писал в главе «О ореховой скорлупе» о потоках, кроме того, что в этом случае не было планирования) следует делать то, что вы хотите.

2
Alex Martelli 7 Июн 2010 в 15:05

Вы вряд ли достигнете предела потока ОС с «несколькими тысячами клиентов»; Вы можете использовать много ненужной памяти со стеками для всех этих потоков.

Посмотрите, что делает Twisted, он позволяет процессу мультиплексировать множество событий (включая таймеры) таким образом, который хорошо работает с большим количеством событий.

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

0
MarkR 7 Июн 2010 в 21:55