400 потоков в 20 процессах превосходят 400 потоков в 4 процессах, выполняя задачу с привязкой к процессору на 4 процессорах - PullRequest
2 голосов
/ 23 мая 2019

Этот вопрос очень похож на 400 потоков в 20 процессах, превосходящих 400 потоков в 4 процессах при выполнении задачи, связанной с вводом / выводом .Единственное отличие состоит в том, что связанный вопрос касается задачи, связанной с вводом-выводом, тогда как этот вопрос касается задачи, связанной с процессором.

Экспериментальный код

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

import math
import multiprocessing
import random
import sys
import time
import threading

def main():
    processes = int(sys.argv[1])
    threads = int(sys.argv[2])
    tasks = int(sys.argv[3])

    # Start workers.
    in_q = multiprocessing.Queue()
    process_workers = []
    for _ in range(processes):
        w = multiprocessing.Process(target=process_worker, args=(threads, in_q))
        w.start()
        process_workers.append(w)

    start_time = time.time()

    # Feed work.
    for nth in range(1, tasks + 1):
        in_q.put(nth)

    # Send sentinel for each thread worker to quit.
    for _ in range(processes * threads):
        in_q.put(None)

    # Wait for workers to terminate.
    for w in process_workers:
        w.join()

    total_time = time.time() - start_time
    task_speed = tasks / total_time

    print('{:3d} x {:3d} workers => {:6.3f} s, {:5.1f} tasks/s'
          .format(processes, threads, total_time, task_speed))



def process_worker(threads, in_q):
    thread_workers = []
    for _ in range(threads):
        w = threading.Thread(target=thread_worker, args=(in_q,))
        w.start()
        thread_workers.append(w)

    for w in thread_workers:
        w.join()


def thread_worker(in_q):
    while True:
        nth = in_q.get()
        if nth is None:
            break
        num = find_nth_prime(nth)
        #print(num)


def find_nth_prime(nth):
    # Find n-th prime from scratch.
    if nth == 0:
        return

    count = 0
    num = 2
    while True:
        if is_prime(num):
            count += 1

        if count == nth:
            return num

        num += 1


def is_prime(num):
    for i in range(2, int(math.sqrt(num)) + 1):
        if num % i == 0:
            return False
    return True


if __name__ == '__main__':
    main()

Вот как я запускаю эту программу:

python3 foo.py <PROCESSES> <THREADS> <TASKS>

Например, python3 foo.py 20 20 2000 создает 20 рабочих процессов с 20 потоками в каждом рабочем процессе (таким образом, в общей сложности 400 рабочих потоков) и выполняет 2000 задач.В конце эта программа печатает, сколько времени потребовалось для выполнения задач и сколько задач в среднем она выполняла в секунду.

Среда

Я тестирую этот код на виртуальной машине Linodeчастный сервер с 8 ГБ оперативной памяти и 4 процессорами.Он работает под управлением Debian 9.

$ cat /etc/debian_version 
9.9

$ python3
Python 3.5.3 (default, Sep 27 2018, 17:25:39) 
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

$ free -m
              total        used        free      shared  buff/cache   available
Mem:           7987          67        7834          10          85        7734
Swap:           511           0         511

$ nproc
4

Пример 1: 20 процессов x 20 потоков

Вот несколько пробных запусков с 400 рабочими потоками, распределенными между 20 рабочими процессами (то есть 20 рабочими потокамив каждом из 20 рабочих процессов).

Вот результаты:

$ python3 bar.py 20 20 2000
 20 x  20 workers => 12.702 s, 157.5 tasks/s

$ python3 bar.py 20 20 2000
 20 x  20 workers => 13.196 s, 151.6 tasks/s

$ python3 bar.py 20 20 2000
 20 x  20 workers => 12.224 s, 163.6 tasks/s

$ python3 bar.py 20 20 2000
 20 x  20 workers => 11.725 s, 170.6 tasks/s

$ python3 bar.py 20 20 2000
 20 x  20 workers => 10.813 s, 185.0 tasks/s

Когда я отслеживаю использование ЦП с помощью команды top, я вижу, что каждый python3 рабочийПроцесс потребляет от 15% до 25% процессорного времени.

Случай 2: 4 процесса x 100 потоков

Теперь я подумал, что у меня всего 4 процессора.Даже если я запускаю 20 рабочих процессов, в любой момент времени физически может работать не более 4 процессов.Кроме того, из-за глобальной блокировки интерпретатора (GIL) только один поток в каждом процессе (таким образом, максимум 4 потока) может работать в любой момент физического времени.

Поэтому я подумал, что если я уменьшу числочисло процессов до 4 и увеличьте число потоков на один процесс до 100, так что общее число потоков все еще останется равным 400, производительность не должна ухудшаться.

Но результаты теста показывают, что 4 процесса, содержащих по 100 потоков каждыйстабильно работают хуже, чем 20 процессов, каждый из которых содержит 20 потоков.

$ python3 bar.py 4 100 2000
  4 x 100 workers => 19.840 s, 100.8 tasks/s

$ python3 bar.py 4 100 2000
  4 x 100 workers => 22.716 s,  88.0 tasks/s

$ python3 bar.py 4 100 2000
  4 x 100 workers => 20.278 s,  98.6 tasks/s

$ python3 bar.py 4 100 2000
  4 x 100 workers => 19.896 s, 100.5 tasks/s

$ python3 bar.py 4 100 2000
  4 x 100 workers => 19.876 s, 100.6 tasks/s

Загрузка ЦП составляет от 50% до 66% для каждого python3 рабочего процесса.

Случай 3: 1 Процесс x400 потоков

Для сравнения, я записываю тот факт, что и случай 1, и случай 2 превосходят тот случай, когда у нас есть все 400 потоков в одном процессе.Это, очевидно, связано с глобальной блокировкой интерпретатора (GIL).

$ python3 bar.py 1 400 2000
  1 x 400 workers => 34.762 s,  57.5 tasks/s

$ python3 bar.py 1 400 2000
  1 x 400 workers => 35.276 s,  56.7 tasks/s

$ python3 bar.py 1 400 2000
  1 x 400 workers => 32.589 s,  61.4 tasks/s

$ python3 bar.py 1 400 2000
  1 x 400 workers => 33.974 s,  58.9 tasks/s

$ python3 bar.py 1 400 2000
  1 x 400 workers => 35.429 s,  56.5 tasks/s

Загрузка ЦП составляет от 110% до 115% для одного рабочего процесса python3.

Случай 4:400 процессов x 1 поток

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

$ python3 bar.py 400 1 2000
400 x   1 workers =>  8.814 s, 226.9 tasks/s

$ python3 bar.py 400 1 2000
400 x   1 workers =>  8.631 s, 231.7 tasks/s

$ python3 bar.py 400 1 2000
400 x   1 workers => 10.453 s, 191.3 tasks/s

$ python3 bar.py 400 1 2000
400 x   1 workers =>  8.234 s, 242.9 tasks/s

$ python3 bar.py 400 1 2000
400 x   1 workers =>  8.324 s, 240.3 tasks/s

Загрузка ЦП находится между 1% к 3% для каждого python3 рабочего процесса.

Сводка

При выборе медианного результата для каждого случая мы получаем следующее резюме:

Case 1:  20 x  20 workers => 12.224 s, 163.6 tasks/s
Case 2:   4 x 100 workers => 19.896 s, 100.5 tasks/s
Case 3:   1 x 400 workers => 34.762 s,  57.5 tasks/s
Case 4: 400 x   1 workers =>  8.631 s, 231.7 tasks/s

Вопрос

Почему 20 процессов x 20 потоков работают лучше, чем 4 процесса x 100 потоков, даже если у меня только 4 процессора?

На самом деле, 400 процессов x 1 поток работает лучше, несмотря на наличие только 4процессоры?Почему?

Ответы [ 2 ]

1 голос
/ 23 мая 2019

Прежде чем поток Python сможет выполнить код, он должен получить Глобальную блокировку интерпретатора (GIL) . Это блокировка на процесс . В некоторых случаях (например, при ожидании завершения операций ввода-вывода) поток обычно освобождает GIL, чтобы другие потоки могли его получить. Если активный поток не снимает блокировку в течение определенного времени, другие потоки могут сигнализировать активному потоку об освобождении GIL, чтобы они могли по очереди.

Имея это в виду, давайте посмотрим, как ваш код работает на моем 4-ядерном ноутбуке:

  1. В простейшем случае (1 процесс с 1 потоком) я получаю ~ 155 задач / с. GIL не мешает нам здесь. Мы используем 100% одного ядра.

  2. Если я увеличу количество потоков (1 процесс с 4 потоками), я получу ~ 70 задач / с. Поначалу это может показаться нелогичным, но это можно объяснить тем фактом, что ваш код в основном связан с процессором, поэтому все потоки нуждаются в GIL почти все время. Только один из них может одновременно выполнять вычисления, поэтому мы не выигрываем от многопоточности. В результате мы используем ~ 25% каждого из моих 4 ядер. Что еще хуже, приобретение и выпуск GIL, а также переключение контекста добавляют значительные издержки, которые снижают общую производительность.

  3. Добавление дополнительных потоков (1 процесс с 400 потоками) не помогает, поскольку одновременно выполняется только один из них. На моем ноутбуке производительность очень похожа на case (2), опять же мы используем ~ 25% каждого из моих 4 ядер.

  4. С 4 процессами с 1 потоком каждый я получаю ~ 550 задач / с. Почти в 4 раза больше, чем я получил в случае (1). На самом деле, немного меньше из-за накладных расходов, необходимых для межпроцессного взаимодействия и блокировки в общей очереди. Обратите внимание, что каждый процесс использует свой собственный GIL.

  5. С 4 процессами, выполняющими по 100 потоков каждый, я получаю ~ 290 задач / с. Снова мы видим замедление, которое мы видели в (2), на этот раз влияющее на каждый отдельный процесс.

  6. С 400 процессами, выполняющими 1 поток каждый, я получаю ~ 530 задач / с. По сравнению с (4) мы видим дополнительные издержки из-за межпроцессного взаимодействия и блокировки в общей очереди.

Пожалуйста, обратитесь к выступлению Дэвида Бизли «Понимание Python GIL» для более подробного объяснения этих эффектов.

Примечание: Некоторые интерпретаторы Python, такие как CPython и PyPy, имеют GIL, в то время как другие, такие как Jython и IronPython, не . Если вы используете другой интерпретатор Python, вы можете увидеть совсем другое поведение.

0 голосов
/ 23 мая 2019

Потоки в Python не выполняются параллельно из-за печально известной глобальной блокировки интерпретатора :

В CPython глобальная блокировка интерпретатора, или GIL, является мьютексом, который защищаетдоступ к объектам Python, не позволяющий нескольким потокам одновременно выполнять байт-коды Python.

Именно поэтому один поток на процесс работает лучше всего в ваших тестах.

Избегайте использования threading.Thread, если действительнопараллельное выполнение важно.

...