【发布时间】:2019-05-23 09:55:36
【问题描述】:
实验代码
下面是实验代码,它可以启动指定数量的工作进程,然后在每个进程内启动指定数量的工作线程并执行获取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
案例 1:20 个进程 x 20 个线程
这里有一些试运行,其中 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。
案例 2:4 个进程 x 100 个线程
现在我以为我只有 4 个 CPU。即使我启动 20 个工作进程,在物理时间的任何时间点最多也只有 4 个进程可以运行。此外,由于全局解释器锁 (GIL),每个进程中只有一个线程(因此最多总共 4 个线程)可以在物理时间的任何时间点运行。
因此,我想如果我将进程数减少到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% 之间。
案例 3:1 个进程 x 400 个线程
只是为了比较,我记录了一个事实,即案例 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% 之间。
案例 4:400 个进程 x 1 个线程
再次,只是为了比较,这里是当有 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 线程好?
【问题讨论】:
-
你真的在测量
urllib.request.urlopen(url),这会导致大量的系统调用、线程(在操作系统意义上)移动到等待状态等等...... -
该方法是纯 IO,因此内核数量并不重要。它正在做一个 Web 应用负载测试器会做的事情。大多数时候该方法是什么都不做,等待操作系统完成网络IO,或者远程服务器响应。时间主要受远程服务器、缓存、网络带宽和网卡性能的影响
-
@PanagiotisKanavos 在这种情况下,我希望 4 个进程 x 100 个线程以及 20 个进程 x 20 个线程执行。那么为什么 4 个进程 x 100 个线程的性能始终比 20 个进程 x 20 个线程差?此外,观察到 1 个进程 x 400 个工作人员的性能最差,这确实表明在物理时间的任何时间点仅使用一个内核会导致性能最差。
-
@SusamPal 为什么要这么期待呢?您首先测量的是什么?这不是 CPU 时间,也不是任何类型的本地性能。你检查你的路由器性能了吗?你的带宽?您的代码不执行任何与 CPU 相关的操作,因此您不是在测量线程性能,而是在测量网络的性能
-
@SusamPal 实际上,进程限制正是导致具有 100 个线程的 4 个进程的工作效率低于 20x20 的原因。如果每个进程都获得相同的限制,那么这 100 个线程就会互相妨碍。
标签: python multithreading performance multiprocessing gil