Ваш код не подходит для сравнения времени запуска между процессами и потоками. Многопоточность Python-код (в CPython) означает одноядерный. Любое выполнение кода Python в одном потоке исключает обработку для всех других потоков в этом процессе, пока этот поток удерживает глобальную блокировку интерпретатора ( GIL ). Это означает, что вы можете иметь параллелизм только с потоками, а не истинный параллелизм, если это касается байт-кода Python.
Ваш пример - это, в основном, тестирование производительности конкретной рабочей нагрузки, связанной с ЦП (выполнение вычислений в узком цикле), что вы не использовали бы потоки в любом случае. Если вы хотите измерить накладные расходы на создание, вы должны лишить себя ничего, кроме самого создания (насколько это возможно).
TL; DR
Запуск потока (тестируется в Ubuntu 18.04) во много раз дешевле, чем запуск процесса.
По сравнению с запуском потока запуск процесса с указанными параметрами start_methods занимает:
- вилка : ~ в 33 раза длиннее
- forkserver : ~ 6693x больше
- spawn : ~ 7558x больше
Полные результаты внизу.
Benchmark
Я недавно обновился до Ubuntu 18.04 и протестировал запуск со скриптом, который, надеюсь, ближе к истине. Обратите внимание, что этот код - Python 3.
Некоторые утилиты для форматирования и сравнения результатов теста:
# thread_vs_proc_start_up.py
import sys
import time
import pandas as pd
from threading import Thread
import multiprocessing as mp
from multiprocessing import Process, Pipe
def format_secs(sec, decimals=2) -> str:
"""Format subseconds.
Example:
>>>format_secs(0.000_000_001)
# Out: '1.0 ns'
"""
if sec < 1e-6:
return f"{sec * 1e9:.{decimals}f} ns"
elif sec < 1e-3:
return f"{sec * 1e6:.{decimals}f} µs"
elif sec < 1:
return f"{sec * 1e3:.{decimals}f} ms"
elif sec >= 1:
return f"{sec:.{decimals}f} s"
def compare(value, base):
"""Return x-times relation of value and base."""
return f"{(value / base):.2f}x"
def display_results(executor, result_series):
"""Display results for Executor."""
exe_str = str(executor).split(".")[-1].strip('\'>')
print(f"\nresults for {exe_str}:\n")
print(result_series.describe().to_string(), "\n")
print(f"Minimum with {format_secs(result_series.min())}")
print("-" * 60)
Эталонный тест функционирует ниже. Для каждого теста из n_runs
создается новая труба.
Запускается новый процесс или поток (исполнитель), и целевая функция calc_start_up_time
немедленно возвращает разницу во времени. Вот и все.
def calc_start_up_time(pipe_in, start):
pipe_in.send(time.perf_counter() - start)
pipe_in.close()
def run(executor, n_runs):
results = []
for _ in range(int(n_runs)):
pipe_out, pipe_in = Pipe(duplex=False)
exe = executor(target=calc_start_up_time, args=(pipe_in,
time.perf_counter(),))
exe.start()
# Note: Measuring only the time for exe.start() returning like:
# start = time.perf_counter()
# exe.start()
# end = time.perf_counter()
# would not include the full time a new process needs to become
# production ready.
results.append(pipe_out.recv())
pipe_out.close()
exe.join()
result_series = pd.Series(results)
display_results(executor, result_series)
return result_series.min()
Сборка запускается из терминала с параметром start_method, а количество прогонов передается в качестве аргументов командной строки. Тест всегда будет запускать n_runs
запуска процесса с указанным параметром start_method (доступно в Ubuntu 18.04: fork, spawn, forkserver), а затем сравнивать с n_runs
запуска потока. Результаты сосредоточены на минимумах, потому что они показывают, насколько быстро это возможно.
if __name__ == '__main__':
# Usage:
# ------
# Start from terminal with start_method and number of runs as arguments:
# $python thread_vs_proc_start_up.py fork 100
#
# Get all available start methods on your system with:
# >>>import multiprocessing as mp
# >>>mp.get_all_start_methods()
start_method, n_runs = sys.argv[1:]
mp.set_start_method(start_method)
mins = []
for executor in [Process, Thread]:
mins.append(run(executor, n_runs))
print(f"Minimum start-up time for processes takes "
f"{compare(*mins)} "
f"longer than for threads.")
Результаты
с n_runs=1000
на моей ржавой машине:
# Ubuntu 18.04 start_method: fork
# ================================
results for Process:
count 1000.000000
mean 0.002081
std 0.000288
min 0.001466
25% 0.001866
50% 0.001973
75% 0.002268
max 0.003365
Minimum with 1.47 ms
------------------------------------------------------------
results for Thread:
count 1000.000000
mean 0.000054
std 0.000013
min 0.000044
25% 0.000047
50% 0.000051
75% 0.000058
max 0.000319
Minimum with 43.89 µs
------------------------------------------------------------
Minimum start-up time for processes takes 33.41x longer than for threads.
# Ubuntu 18.04 start_method: spawn
# ================================
results for Process:
count 1000.000000
mean 0.333502
std 0.008068
min 0.321796
25% 0.328776
50% 0.331763
75% 0.336045
max 0.415568
Minimum with 321.80 ms
------------------------------------------------------------
results for Thread:
count 1000.000000
mean 0.000056
std 0.000016
min 0.000043
25% 0.000046
50% 0.000048
75% 0.000065
max 0.000231
Minimum with 42.58 µs
------------------------------------------------------------
Minimum start-up time for processes takes 7557.80x longer than for threads.
# Ubuntu 18.04 start_method: forkserver
# =====================================
results for Process:
count 1000.000000
mean 0.295011
std 0.007157
min 0.287871
25% 0.291440
50% 0.293263
75% 0.296185
max 0.361581
Minimum with 287.87 ms
------------------------------------------------------------
results for Thread:
count 1000.000000
mean 0.000055
std 0.000014
min 0.000043
25% 0.000045
50% 0.000047
75% 0.000064
max 0.000251
Minimum with 43.01 µs
------------------------------------------------------------
Minimum start-up time for processes takes 6693.44x longer than for threads.