在 Linux 上创建线程与进程的开销
Posted
技术标签:
【中文标题】在 Linux 上创建线程与进程的开销【英文标题】:Overhead in creating a thread vs process on Linux 【发布时间】:2019-02-10 00:33:40 【问题描述】:我试图回答在 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 上专门针对数千个网络请求使用多线程或多处理是否合适。似乎根据这段代码,它们在启动时间方面是相同的,但也许进程在内存使用方面要重得多?
【问题讨论】:
收益递减 - 在单台机器上,300 'workers' 的值仅适用于特定的 低 CPU、高 IO 情况,甚至可能不适用然后(取决于共享锁和代码时间,例如;请参阅 GIL 以了解仅此一项可能如何改变进程与线程的选择)。示例中的“工作量”也是可疑和误导性的:即,是否所有的“工人”都同时排队?有了更合适的工作人员(例如“实际代码”),就有可能以随意的方式分析其他系统资源的使用情况.. 我敢于质疑 Python 2 是否也适合。也就是说,停止编写课程:youtube.com/watch?v=o9pEzgHorH0。线程和进程都不需要创建派生类。 【参考方案1】:您的代码不适合对进程和线程之间的启动时间进行基准测试。多线程 Python 代码(在 CPython 中)意味着单核。在该线程持有全局解释器锁 (GIL) 期间,一个线程中的任何 Python 代码执行都将排除该进程中所有其他线程的继续执行。这意味着只要涉及 Python 字节码,您就只能与线程进行并发,而不是真正的并行。
您的示例主要是对特定的受 CPU 限制的工作负载性能进行基准测试(在紧密循环中运行计算),无论如何您都不会使用线程。如果你想测量创建开销,你必须从你的基准测试中去除除创建本身之外的任何东西(尽可能)。
TL;博士
启动线程(在 Ubuntu 18.04 上进行基准测试)比启动进程便宜很多倍。
与线程启动相比,以指定的 start_methods 启动进程需要:
分叉:约 33 倍长 forkserver:约 6693 倍长 生成:约 7558 倍长完整的结果在底部。
基准测试
我最近升级到 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:.decimalsf ns"
elif sec < 1e-3:
return f"sec * 1e6:.decimalsf µs"
elif sec < 1:
return f"sec * 1e3:.decimalsf ms"
elif sec >= 1:
return f"sec:.decimalsf s"
def compare(value, base):
"""Return x-times relation of value and base."""
return f"(value / base):.2fx"
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 启动的进程的n_runs
(在 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.
【讨论】:
【参考方案2】:这取决于......也许“两者”可能是您正在寻找的答案。
python 中的多进程使用 linux 中的标准 fork() 调用来复制主进程。在您的最小程序的情况下,这可能不是很多数据,但是根据最终程序的结构,可以说是更多的数据。在最小的情况下,进程内存开销非常小。
线程不会有这个内存开销问题,但是除了启动时间之外,它还有另一个潜在的问题,你可能不得不担心...... GIL。如果您的线程在很大程度上被阻塞以等待 I/O,则 GIL 可能不会成为问题,但是如果您只是像在测试中那样运行循环,则一次只能运行 2 个线程....
换句话说;即使您在测试中获得了相同的时间,但在幕后发生了很多事情,像这样的简单测试是无法捕捉到的。
一个正在运行的程序的正确答案可能是不担心启动时间,但它可能更多地依赖于
每个线程或进程要做什么? 它需要访问哪些内存和状态,锁定是否会成为问题? 在 python 中,GIL 是否会对工作负载产生问题(一次运行 2 个线程是否足以满足工作负载) 将进程占用空间乘以进程数是否为可接受的内存量我遵循的一个基本经验法则是,如果线程/进程主要在 I/O 上被阻塞(等待网络流量或其他东西),请使用线程。如果您有更多计算量大的需求并且内存不是问题,请使用进程。
该规则的一个例外是我要如何处理进程或线程的内存和状态。当您开始谈论大量线程和此类进程时,您可能会考虑内存访问/锁定争用...
但实际上,如果没有更多数据,很难提出好的建议。并发编程是很多人都会做的事情之一,但很少有人真正理解得很好(根据我的经验)。
需要研究的一些额外内容可能是重组进程以需要更少的线程。通常,在制作网络服务器和客户端时,我最终会使用线程并有一个侦听器和发送者线程,它们要么阻塞在队列上,要么阻塞在等待某事做的套接字上。您可能希望减少仅提供队列的侦听器和发送者,从而限制开销。我认为 Python3.5+ 中有一个新的 asyncio 库也可以简化你的生活。
我知道我并没有真正回答你的问题,但我希望我提供了一些东西来查找和检查。
希望有帮助!
【讨论】:
【参考方案3】:要回答您的问题,我们需要了解 Python 中的线程和多处理的一些基础知识。事实证明,问题不在于启动开销,而在于每个人如何将运行负载分配到系统资源上。
首先,python 中的线程与 Linux 中的线程不同。 Linux 为每个线程创建一个新的轻量级进程,这些进程可以在不同的 CPU 内核上运行,而 python 脚本及其线程在任何给定时刻都在同一个 CPU 内核中运行。如果你想在python中实现真正的多处理,你必须使用多处理接口。
为了演示以上内容,运行 Linux 系统监视器,选择资源选项卡,然后在不同的终端窗口中,尝试运行我在下面插入的两个代码 sn-ps 中的每一个。资源选项卡显示每个 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)
【讨论】:
以上是关于在 Linux 上创建线程与进程的开销的主要内容,如果未能解决你的问题,请参考以下文章