以下是实验代码,可以启动指定数量的工作进程,然后在每个进程中启动指定数量的工作线程,并执行获取URL的任务:
import multiprocessing
import sys
import time
import threading
import urllib.request
def main():
processes = int(sys.argv[1])
threads = int(sys.argv[2])
urls = int(sys.argv[3])
# Start process 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 n in range(urls):
in_q.put('http://www.example.com/?n={}'.format(n))
# 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()
# Print time consumed and fetch speed.
total_time = time.time() - start_time
fetch_speed = urls / total_time
print('{} x {} workers => {:.3} s, {:.1f} URLs/s'
.format(processes, threads, total_time, fetch_speed))
def process_worker(threads, in_q):
# Start thread workers.
thread_workers = []
for _ in range(threads):
w = threading.Thread(target=thread_worker, args=(in_q,))
w.start()
thread_workers.append(w)
# Wait for thread workers to terminate.
for w in thread_workers:
w.join()
def thread_worker(in_q):
# Each thread performs the actual work. In this case, we will assume
# that the work is to fetch a given URL.
while True:
url = in_q.get()
if url is None:
break
with urllib.request.urlopen(url) as u:
pass # Do nothing
# print('{} - {} {}'.format(url, u.getcode(), u.reason))
if __name__ == '__main__':
main()
这是我运行此程序的方式:
python3 foo.py <PROCESSES> <THREADS> <URLS>
例如,python3 foo.py 20 20 10000
创建20个工作进程,每个工作进程中有20个线程(因此共有400个工作线程),并获取10000个URL。最后,该程序将打印获取URL所需的时间以及平均每秒获取多少URL。
请注意,在所有情况下,我实际上都访问了www.example.com
域的URL,即www.example.com
不仅仅是一个占位符。换句话说,我未修改地运行了上面的代码。
我正在具有8 GB RAM和4个CPU的Linode虚拟专用服务器上测试此代码。它正在运行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
这里有一些试运行,其中400个工作线程分布在20个工作进程之间(即20个工作进程中的每个工作进程中有20个工作线程)。在每个试验中,都会提取10,000个URL。
以下是结果:
$ python3 foo.py 20 20 10000
20 x 20 workers => 5.12 s, 1954.6 URLs/s
$ python3 foo.py 20 20 10000
20 x 20 workers => 5.28 s, 1895.5 URLs/s
$ python3 foo.py 20 20 10000
20 x 20 workers => 5.22 s, 1914.2 URLs/s
$ python3 foo.py 20 20 10000
20 x 20 workers => 5.38 s, 1859.8 URLs/s
$ python3 foo.py 20 20 10000
20 x 20 workers => 5.19 s, 1925.2 URLs/s
我们可以看到平均每秒大约获取1900个URL。当我使用top
命令监视CPU使用率时,我发现每个python3
工作进程都消耗大约10%到15%的CPU。
现在我认为我只有4个CPU。即使我启动了20个工作进程,在物理时间的任何时候最多也只能运行4个进程。此外,由于全局解释器锁(GIL),每个进程中只能有一个线程(因此最多四个线程)可以在物理时间的任何时间运行。
因此,我认为如果将进程数减少到4,并将每个进程的线程数增加到100,以便线程总数仍然保持400,则性能应该不会降低。
但是测试结果显示,每个包含100个线程的4个进程的性能始终比每个包含20个线程的20个进程的性能差。
$ python3 foo.py 4 100 10000
4 x 100 workers => 9.2 s, 1086.4 URLs/s
$ python3 foo.py 4 100 10000
4 x 100 workers => 10.9 s, 916.5 URLs/s
$ python3 foo.py 4 100 10000
4 x 100 workers => 7.8 s, 1282.2 URLs/s
$ python3 foo.py 4 100 10000
4 x 100 workers => 10.3 s, 972.3 URLs/s
$ python3 foo.py 4 100 10000
4 x 100 workers => 6.37 s, 1570.9 URLs/s
每个python3
工作进程的CPU使用率在40%到60%之间。
为了进行比较,我记录了一个事实,即情况1和情况2均优于单个进程中所有400个线程的情况。这肯定是由于全局解释器锁(GIL)。
$ python3 foo.py 1 400 10000
1 x 400 workers => 13.5 s, 742.8 URLs/s
$ python3 foo.py 1 400 10000
1 x 400 workers => 14.3 s, 697.5 URLs/s
$ python3 foo.py 1 400 10000
1 x 400 workers => 13.1 s, 761.3 URLs/s
$ python3 foo.py 1 400 10000
1 x 400 workers => 15.6 s, 640.4 URLs/s
$ python3 foo.py 1 400 10000
1 x 400 workers => 13.1 s, 764.4 URLs/s
单个python3
工作进程的CPU使用率介于120%和125%之间。
同样,为了比较,这是当有400个进程(每个进程都有一个线程)时结果的外观。
$ python3 foo.py 400 1 10000
400 x 1 workers => 14.0 s, 715.0 URLs/s
$ python3 foo.py 400 1 10000
400 x 1 workers => 6.1 s, 1638.9 URLs/s
$ python3 foo.py 400 1 10000
400 x 1 workers => 7.08 s, 1413.1 URLs/s
$ python3 foo.py 400 1 10000
400 x 1 workers => 7.23 s, 1382.9 URLs/s
$ python3 foo.py 400 1 10000
400 x 1 workers => 11.3 s, 882.9 URLs/s
每个python3
工作进程的CPU使用率在1%到3%之间。
从每个案例中选择中位数结果,我们得到以下摘要:
Case 1: 20 x 20 workers => 5.22 s, 1914.2 URLs/s ( 10% to 15% CPU/process)
Case 2: 4 x 100 workers => 9.20 s, 1086.4 URLs/s ( 40% to 60% CPU/process)
Case 3: 1 x 400 workers => 13.5 s, 742.8 URLs/s (120% to 125% CPU/process)
Case 4: 400 x 1 workers => 7.23 s, 1382.9 URLs/s ( 1% to 3% CPU/process
即使我只有4个CPU,为什么20个进程x 20个线程的性能要比4个进程x 100个线程好?
答案 0 :(得分:2)
您的任务是I / O绑定的,而不是CPU绑定的:线程大部分时间都在睡眠状态下等待网络数据等,而不是使用CPU。
因此,只要I / O仍然是瓶颈,在这里添加比CPU多的线程是可行的。只有线程数量太多,一次足以准备好开始主动竞争CPU周期时(或者当网络带宽耗尽时,以先到者为准),这种影响才会减弱。
为什么每个进程20个线程快于每个进程100个线程:这很可能是CPython的GIL造成的。在处理I / O时,Python机制:
如果同一进程中有足够的线程,则到达第4步时,另一个线程处于活动状态的可能性就会增加,从而导致额外的随机延迟。
现在,当涉及到很多进程时,其他因素也会像内存交换一样起作用(由于与线程不同,因为运行相同代码的进程不会共享内存)(我很确定很多其他因素也会导致延迟)进程而不是线程争夺资源,但是无法从头顶上指出这一点)。这就是性能变得不稳定的原因。