我试图回答在Python中创建线程与进程相比有多少开销的问题。我从一个类似的问题修改了代码,该问题基本上是通过两个线程运行一个函数,然后通过两个进程运行相同的函数并报告时间。
import time, sys
NUM_RANGE = 100000000
from multiprocessing import Process
import threading
def timefunc(f):
t = time.time()
f()
return time.time() - t
def multiprocess():
class MultiProcess(Process):
def __init__(self):
Process.__init__(self)
def run(self):
# Alter string + test processing speed
for i in xrange(NUM_RANGE):
a = 20 * 20
for _ in xrange(300):
MultiProcess().start()
def multithreading():
class MultiThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
# Alter string + test processing speed
for i in xrange(NUM_RANGE):
a = 20 * 20
for _ in xrange(300):
MultiThread().start()
print "process run time" + str(timefunc(multiprocess))
print "thread run time" + str(timefunc(multithreading))
然后我得到7.9s的多处理和7.9s的多线程
我要回答的主要问题是,是否适合在Linux上针对数千个网络请求使用多线程或多处理。似乎根据此代码,它们在启动时间方面是相同的,但也许进程的内存使用量要大得多?
答案 0 :(得分:0)
这取决于...也许“两者”都可能是您想要的答案。
Python中的多进程在Linux中使用标准的fork()调用来复制主进程。就您的最小程序而言,这可能不是很多数据,但是可以说,取决于最终程序的结构,可能还有更多数据需要分叉。在最小的情况下,进程的内存开销非常小。
线程不会有此内存开销问题,但除了启动时间外,它还有另一个潜在问题,您可能要担心... GIL。如果在等待I / O的过程中很大程度上阻塞了GIL,那么GIL可能不会成为问题,但是如果您像在测试中那样仅运行一个循环,则一次只能运行2个线程。 / p>
换句话说;即使您在测试中有相同的时间,但在幕后进行的很多事情都无法阻止像这样的简单测试。
对于正在运行的程序,正确的答案可能不必担心启动时间,但可能会更多地依赖于
我遵循的基本经验法则是,如果线程/进程主要将在I / O(等待网络通信或其他)上被阻塞,则使用线程。如果您对计算的要求更高,而不必担心内存,请使用一个过程。
该规则的一个例外是我想如何处理进程或线程的内存和状态。当您开始谈论大量线程以及类似的进程时,您可能会考虑内存访问/锁争用...
实际上,如果没有更多数据,很难提出一个好的建议。并发编程是很多人要做的事情之一,但根据我的经验,很少有人真正了解。
需要研究的其他内容可能是重组过程,以减少线程数量。通常,在制作网络服务器和客户端时,我最终会使用线程,并且只有一个侦听器和发送器线程,它们要么在队列中阻塞,要么在套接字上等待执行操作。您可能希望有更少的侦听器和发送器仅用于填充队列,从而限制了开销。我认为Python3.5 +中有一个新的asyncio库,它也可以简化您的生活。
我知道我并没有真正回答你的问题,但是我希望我提供一些东西可以查找并签出。
希望有帮助!
答案 1 :(得分:0)
要回答您的问题,我们需要了解python中的线程和多处理的一些基础知识。事实证明,问题不仅仅在于启动开销,而是每个组件如何在系统资源上分配运行负载。
首先,python中的线程与Linux中的线程不同。 Linux为每个线程创建一个新的轻量级进程,并且这些进程可以在不同的CPU内核上运行,而python脚本及其线程都可以在任何给定的瞬间在同一CPU内核上运行。如果要在python中进行真正的多重处理,则必须使用多重处理界面。
为演示上述内容,请运行Linux系统监视器,选择“资源”选项卡,然后在另一个终端窗口中,尝试运行我在下面插入的两个代码段中的每个。资源选项卡显示了每个CPU内核上的负载。
第二个重要问题是您要同时处理数千个传入连接。为此,您可能需要多处理接口,但是在Linux中配置时,或者在调度或资源瓶颈中,可能会限制可以容纳多少个进程和连接,请参见c.f。硬件。
如果选择一次不具有大量活动的进程,则处理此问题的一种方法是创建固定数量的进程,将它们存储在列表中,然后将传入连接传递给它们它们进入。当所有进程都忙时,您等待。为此,您至少需要一个计数信号量。
如果要在建立连接时创建进程,则可以再次使用计数信号量来限制一次运行的进程数。您可以将计数信号量初始化为最大数量,针对创建的每个进程将其减少,并在进程退出时增加它。如上所述,当达到允许的最大进程数时,请等待。
好的,这是线程和多处理的代码示例。第一个启动5个线程。第二个开始5个过程。您可以通过一次编辑更改它们,以达到100、1000等的方式。每个整数处理循环,让您查看Linux系统监控程序中的负载。
#!/usr/bin/python
# Parallel code with shared variables, using threads
from threading import Lock, Thread
from time import sleep
# Variables to be shared across threads
counter = 0
run = True
lock = Lock()
# Function to be executed in parallel
def myfunc():
# Declare shared variables
global run
global counter
global lock
# Processing to be done until told to exit
while run:
n = 0
for i in range(10000):
n = n+i*i
print( n )
sleep( 1 )
# Increment the counter
lock.acquire()
counter = counter + 1
lock.release()
# Set the counter to show that we exited
lock.acquire()
counter = -1
lock.release()
print( 'thread exit' )
# ----------------------------
# Launch the parallel function in a set of threads
tlist = []
for n in range(5):
thread = Thread(target=myfunc)
thread.start()
tlist.append(thread)
# Read and print the counter
while counter < 5:
print( counter )
n = 0
for i in range(10000):
n = n+i*i
print( n )
#sleep( 1 )
# Change the counter
lock.acquire()
counter = 0
lock.release()
# Read and print the counter
while counter < 5:
print( counter )
n = 0
for i in range(10000):
n = n+i*i
print( n )
#sleep( 1 )
# Tell the thread to exit and wait for it to exit
run = False
for thread in tlist:
thread.join()
# Confirm that the thread set the counter on exit
print( counter )
这是多处理版本:
#!/usr/bin/python
from time import sleep
from multiprocessing import Process, Value, Lock
def myfunc(counter, lock, run):
while run.value:
sleep(1)
n=0
for i in range(10000):
n = n+i*i
print( n )
with lock:
counter.value += 1
print( "thread %d"%counter.value )
with lock:
counter.value = -1
print( "thread exit %d"%counter.value )
# -----------------------
counter = Value('i', 0)
run = Value('b', True)
lock = Lock()
plist = []
for n in range(5):
p = Process(target=myfunc, args=(counter, lock, run))
p.start()
plist.append(p)
while counter.value < 5:
print( "main %d"%counter.value )
n=0
for i in range(10000):
n = n+i*i
print( n )
sleep(1)
with lock:
counter.value = 0
while counter.value < 5:
print( "main %d"%counter.value )
sleep(1)
run.value = False
for p in plist:
p.join()
print( "main exit %d"%counter.value)
答案 2 :(得分:0)
您的代码不适合基准测试进程和线程之间的启动时间。多线程Python代码(在CPython中)表示单核。在一个线程持有全局解释器锁(GIL)的时间内,一个线程中的任何Python代码执行都将排除该进程中所有其他线程的继续。这意味着您只能与线程并发,而不能真正的并行,只要它涉及Python字节码即可。
您的示例主要是测试特定于CPU的工作负载性能(在紧凑的循环中运行计算),无论如何您都不会使用线程。如果要衡量创建开销,则必须从基准中除去创建本身之外的所有内容。
TL; DR
启动线程(在Ubuntu 18.04上标有基准)比启动进程便宜很多倍。
与线程启动相比,使用指定的start_methods启动进程需要:
完整结果在底部。
基准
我最近升级到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和作为命令行参数传递的运行次数从终端启动的。基准测试将始终以指定的start_method(在Ubuntu 18.04上提供:fork,spawn,forkserver)启动的进程运行n_runs
,然后与线程启动的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.