Python多线程与多进程浅析之二

简介: Python 多线程 Step by Step Python 在 CPU 密集运算的场景,多个线程并不能提高太多性能,而对于 I/O 阻塞的场景,可以使得运行效率获得几倍的提高。我们接下来会详细的分析一下。

Python 多线程 Step by Step

Python 在 CPU 密集运算的场景,多个线程并不能提高太多性能,而对于 I/O 阻塞的场景,可以使得运行效率获得几倍的提高。我们接下来会详细的分析一下。

我们先做一个可以用来测试的基准程序,这是一个比较无聊的计算程序,可以理解为是一个CPU 密集型的测试。当然你也可以换做找最大公约数、求质数或者读者自己的计算程序。

在写这部分内容的时候,我的代码是在 Jupyter 中执行的,这是一台 2012年秋天款式的 mac mini,2.3GHz Intel Core i7, 一个处理器4个核心,16 G 1600 MHz DDR3 内存。

测试时间,我们这里就简单的取一次值,对于 CPU 密集运算的情况来说,除非电脑有更加消耗资源的应用,一般差异不大,但是和其他比如磁盘、网络等相关的应用就不一定了。

# 计算一次 my_cal() 函数,获得耗用时间
>>> from time import time
>>> def my_cal(a):
...     j = 0
...     for i in range(a):
...         j = j + i
...     print(j)
...     return j

>>> start = time()
>>> my_cal(1000000)
>>> end = time()
>>> print('cost time {:f} s'.format(end - start))
499999500000 
cost time 0.106146 s

真实场景都要复杂得多,我们为了后面的例子,用一个 list 来提供数据源,使用上面的累加函数来计算 5 次。

>>> from time import time

>>> def my_cal(a):
...     j = 0
...     for i in range(a):
...         j = j + i
...     print(j)
...     return j

>>> start = time()
>>> list_01 = [1000000, 1500000, 2000000, 2500000, 3000000]

>>> for i in list_01:
...    my_cal(i)
    
>>> end = time()
>>> print('cost time {:f} s'.format(end - start))  
499999500000
1124999250000
1999999000000
3124998750000
4499998500000
cost time 0.906054 s

当然我们可以写的更加简洁一些,用 map() 函数是最合适在这样的场景了。所以读者如果要测试的话,可以用上面循环的方式,也可以用下面 map() 的方式,看具体情况。整体上来说推荐 Python 风格的写法,能用 map() 的尽量就不要用 for 循环了。

>>> from time import time

>>> def my_cal(a):
...     j = 0
...     for i in range(a):
...         j = j + i
...     print(j)
...     return j

>>> start = time()

>>> list_01 = [1000000, 1500000, 2000000, 2500000, 3000000]
>>> list_02 = list(map(my_cal, list_01))
        
>>> end = time()
>>> print('cost time {:f} s'.format(end - start))
499999500000
1124999250000
1999999000000
3124998750000
4499998500000
cost time 0.927783 s

我们先用简单的多线程的方式,来看看有没有提速的作用。

>>> from time import time
>>> import threading

>>> def my_cal(a):
...     j = 0
...     for i in range(a):
...         j = j + i
...     print(j)
...     return j

>>> start = time()
>>> list_01 = [1000000, 1500000, 2000000, 2500000, 3000000]
>>> Threads = []

>>> for i in list_01:
...     # 建立线程
...     thread = threading.Thread(target=my_cal,args=(i,))
...     thread.start()
...     # 建立线程列表
...     Threads.append(thread)

# 等待线程结束
>>> for thread in Threads:
...     thread.join()
       
>>> end = time()
>>> print('\ncost time {:f} s'.format(end - start))   
499999500000
1124999250000
1999999000000
3124998750000
4499998500000

cost time 1.074110 s

上面这样写,可以执行多个线程。但是如前面所言,因为实际上还是在不同的线程中轮流,对于 CPU 密集型的例子来说,多线程对性能提升是没有什么用处,还让管理线程占用多一点的资源。

注意,创建的 thread 只能执行一次 start() 方法,否则会报错: RuntimeError: threads can only be started once
thread 的 join() 方法阻塞当前线程,等待子线程执行完毕后自己再继续执行。如果没有 join() 的阻塞,我们会先看到 cost time 的计算,而实际上上面的线程还没有完成自己的任务。

多线程多 join 的情况下,依次执行各线程的join方法,前面一个结束了才能执行后面一个。

注意:thread 模块在 Python 3 中已被废弃,用 threading 模块代替。在 Python3 中不能再使用"thread" 模块。为了兼容性,Python3 将 thread 重命名为 "_thread"。

# 检查线程的状态
>>> from time import time
>>> import threading

>>> def my_cal(a):
...     j = 0
...     for i in range(a):
...         j = j + i
...     print(j)
...     return j

>>> thread = threading.Thread(target=my_cal,args=(i,))
# 创建后的线程的状态
>>> print(thread.isAlive)
>>> thread.start()
# 线程启动后的状态
>>> print(thread.isAlive)   
<bound method Thread.is_alive of <Thread(Thread-43, initial)>>
<bound method Thread.is_alive of <Thread(Thread-43, started 123145451360256)>>
4499998500000

多进程方式

对于 CPU 运算密集的场景,我们换做多进程的方式来看一下。

# 多进程方式
>>> from time import time
>>> from concurrent.futures import *

>>> def my_cal(a):
...     j = 0
...     for i in range(a):
...         j = j + i
...     print(j)
...     return j

>>> list_01 = [1000000, 2000000, 1500000, 2500000, 3000000]
>>> start = time()
>>> pool = ProcessPoolExecutor(max_workers=10)
>>> list_02 = list(pool.map(my_cal, list_01))
>>> end = time()

>>> print('cost time {:f} s'.format(end - start)) 
499999500000
1124999250000
1999999000000
3124998750000
4499998500000
cost time 0.374396 s

我们可以看到速度提升了60%以上。我觉得这是提高性能最好和最简单的方法之一。

设置多少个 worker,一般是等于 cpu 核心数或者乘以二,服务器 Gunicorn worker 的数量从经验的角度一般配置 2 * core + 1, core指的核心数。

修改一下程序,用 timeit 来进行测试。(注意,请在Jupyter中运行该程序)

# 多进程方式,测试 workers

from concurrent.futures import *

def my_cal(a):
    j = 0
    for i in range(a):
        j = j + i
    return j

def my_cal_main(workers):

    list_01 = [1000000, 2000000, 1500000, 2500000, 3000000]
    pool = ProcessPoolExecutor(workers)
    list_02 = list(pool.map(my_cal, list_01))

for i in range(2,11):
    print(i)
%timeit -n6 my_cal_main(i)

# 输出结果
2
6 loops, best of 3: 491 ms per loop
3
6 loops, best of 3: 443 ms per loop
4
6 loops, best of 3: 437 ms per loop
5
6 loops, best of 3: 352 ms per loop
6
6 loops, best of 3: 336 ms per loop
7
6 loops, best of 3: 341 ms per loop
8
6 loops, best of 3: 346 ms per loop
9
6 loops, best of 3: 346 ms per loop
10
6 loops, best of 3: 347 ms per loop

我们可以看到,worker 设为 6 的时候速度最快,再往上没有明显差别。这个还和应用场景有关,所以如果不想太复杂的化,设为 CPU 核心数比较适合。

下面的有三种方法可以获得 CPU 核心的数量:

# 获得 cpu 核心数量
>>> import multiprocessing
>>> pool = multiprocessing.Pool()
>>> print(pool._processes)
8
>>> print(multiprocessing.cpu_count())
8

>>> import psutil
>>> psutil.cpu_count()
8

摘自本人与同事所著《Python 机器学习实战》一书

目录
相关文章
|
1月前
|
调度 开发者 Python
深入浅出操作系统:进程与线程的奥秘
在数字世界的底层,操作系统扮演着不可或缺的角色。它如同一位高效的管家,协调和控制着计算机硬件与软件资源。本文将拨开迷雾,深入探索操作系统中两个核心概念——进程与线程。我们将从它们的诞生谈起,逐步剖析它们的本质、区别以及如何影响我们日常使用的应用程序性能。通过简单的比喻,我们将理解这些看似抽象的概念,并学会如何在编程实践中高效利用进程与线程。准备好跟随我一起,揭开操作系统的神秘面纱,让我们的代码运行得更加流畅吧!
|
1月前
|
消息中间件 Unix Linux
【C语言】进程和线程详解
在现代操作系统中,进程和线程是实现并发执行的两种主要方式。理解它们的区别和各自的应用场景对于编写高效的并发程序至关重要。
57 6
|
1月前
|
调度 开发者
深入理解:进程与线程的本质差异
在操作系统和计算机编程领域,进程和线程是两个核心概念。它们在程序执行和资源管理中扮演着至关重要的角色。本文将深入探讨进程与线程的区别,并分析它们在现代软件开发中的应用和重要性。
59 5
|
30天前
|
算法 调度 开发者
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
|
1月前
|
调度 开发者
核心概念解析:进程与线程的对比分析
在操作系统和计算机编程领域,进程和线程是两个基本而核心的概念。它们是程序执行和资源管理的基础,但它们之间存在显著的差异。本文将深入探讨进程与线程的区别,并分析它们在现代软件开发中的应用和重要性。
56 4
|
2月前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
2月前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
2月前
|
监控 JavaScript 前端开发
python中的线程和进程(一文带你了解)
欢迎来到瑞雨溪的博客,这里是一位热爱JavaScript和Vue的大一学生分享技术心得的地方。如果你从我的文章中有所收获,欢迎关注我,我将持续更新更多优质内容,你的支持是我前进的动力!🎉🎉🎉
29 0
|
2月前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
69 0
|
8月前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。