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天前
|
Java 测试技术 Python
Python的多线程允许在同一进程中并发执行任务
【5月更文挑战第17天】Python的多线程允许在同一进程中并发执行任务。示例1展示了创建5个线程打印&quot;Hello World&quot;,每个线程调用同一函数并使用`join()`等待所有线程完成。示例2使用`ThreadPoolExecutor`下载网页,创建线程池处理多个URL,打印出每个网页的大小。Python多线程还可用于线程间通信和同步,如使用Queue和Lock。
13 1
|
2天前
|
数据处理 Python
Python并发编程:实现高效的多线程与多进程
Python作为一种高级编程语言,提供了强大的并发编程能力,通过多线程和多进程技术,可以实现程序的并发执行,提升系统的性能和响应速度。本文将介绍Python中多线程和多进程的基本概念,以及如何利用它们实现高效的并发编程,解决实际开发中的并发性问题。
|
3天前
|
Java Python
Python 内置库 多线程threading使用讲解
本文介绍Python中的线程基础。首先展示了单线程的基本使用,然后通过`threading`模块创建并运行多线程。示例中创建了两个线程执行不同任务,并使用`active_count()`和`enumerate()`检查线程状态。接着讨论了守护线程,主线程默认等待所有子线程完成,但可设置子线程为守护线程使其随主线程一同结束。`join()`方法用于主线程阻塞等待子线程执行完毕,而线程池能有效管理线程,减少频繁创建的开销,Python提供`ThreadPoolExecutor`进行线程池操作。最后提到了GIL(全局解释器锁),它是CPython的机制,限制了多线程并行执行的能力,可能导致性能下降。
12 1
|
3天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
3天前
|
缓存 负载均衡 安全
在Python中,如何使用多线程或多进程来提高程序的性能?
【2月更文挑战第17天】【2月更文挑战第50篇】在Python中,如何使用多线程或多进程来提高程序的性能?
|
3天前
|
安全 Python
Python中的并发编程:多线程与多进程技术探究
本文将深入探讨Python中的并发编程技术,重点介绍多线程和多进程两种并发处理方式的原理、应用场景及优缺点,并结合实例分析如何在Python中实现并发编程,以提高程序的性能和效率。
|
3天前
|
数据采集 数据库 C++
python并发编程:并发编程中是选择多线程呢?还是多进程呢?还是多协程呢?
python并发编程:并发编程中是选择多线程呢?还是多进程呢?还是多协程呢?
22 0
|
3天前
|
消息中间件 安全 调度
Python从入门到精通:3.1.1多线程与多进程——进程和线程的概念
Python从入门到精通:3.1.1多线程与多进程——进程和线程的概念
|
3天前
|
调度 Python
Python多线程、多进程与协程面试题解析
【4月更文挑战第14天】Python并发编程涉及多线程、多进程和协程。面试中,对这些概念的理解和应用是评估候选人的重要标准。本文介绍了它们的基础知识、常见问题和应对策略。多线程在同一进程中并发执行,多进程通过进程间通信实现并发,协程则使用`asyncio`进行轻量级线程控制。面试常遇到的问题包括并发并行混淆、GIL影响多线程性能、进程间通信不当和协程异步IO理解不清。要掌握并发模型,需明确其适用场景,理解GIL、进程间通信和协程调度机制。
38 0