在Linux上创建线程与进程的开销

时间:2018-09-05 00:02:20

标签: python linux multithreading multiprocessing

我试图回答在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上针对数千个网络请求使用多线程或多处理。似乎根据此代码,它们在启动时间方面是相同的,但也许进程的内存使用量要大得多?

3 个答案:

答案 0 :(得分:0)

这取决于...也许“两者”都可能是您想要的答案。

Python中的多进程在Linux中使用标准的fork()调用来复制主进程。就您的最小程序而言,这可能不是很多数据,但是可以说,取决于最终程序的结构,可能还有更多数据需要分叉。在最小的情况下,进程的内存开销非常小。

线程不会有此内存开销问题,但除了启动时间外,它还有另一个潜在问题,您可能要担心... GIL。如果在等待I / O的过程中很大程度上阻塞了GIL,那么GIL可能不会成为问题,但是如果您像在测试中那样仅运行一个循环,则一次只能运行2个线程。 / p>

换句话说;即使您在测试中有相同的时间,但在幕后进行的很多事情都无法阻止像这样的简单测试。

对于正在运行的程序,正确的答案可能不必担心启动时间,但可能会更多地依赖于

  • 每个线程或进程将要做什么?
  • 它需要访问什么内存和状态,锁定会成为问题吗?
  • 在python中,GIL是否会对工作负载造成问题(一次运行2个线程足以应付工作负载)
  • 将进程足迹乘以进程数即可得到可接受的内存量

我遵循的基本经验法则是,如果线程/进程主要将在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启动进程需要:

  • :长约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:.{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.