在自动化测试领域,多线程和多进程技术被广泛应用于提高测试的执行效率和性能。通过并发运行测试用例,可以显著缩短测试周期,特别是在面对大量测试用例或者需要在多个环境中进行测试时尤为重要。
在实际的自动化测试中,我们经常碰到类似的需求:
-
100条自动化测试用例,采用默认的依次执行方式,所需要的时间竟然高达1个小时,我们需要想办法对其进行时间上的优化
-
App自动化测试中,我们需要同时对多台测试机进行同时测试,以满足我们对于兼容性测试的需求
-
自动化测试中,我们需要额外监控应用程序/测试机的性能(比如CPU、内存的使用情况),监控不能干扰主测试流程
在这个时候往往我们需要使用并发编程的技术-多线程和多进程,通过并发执行测试用例,可以显著缩短自动化测试时间,同时也能实现在多个环境中同时执行测试的效果。
那么到底什么是多进程和多线程,以及什么时候选择多进程什么时候选择多线程呢?
Python多线程
在计算机中线程是执行中的最小单位。一个进程中可以包含多个线程,它们共享进程的资源,包括内存空间和打开的文件。每个线程都有自己的执行路径,可以独立执行任务。线程的创建和管理由操作系统负责,而不同的操作系统可能对线程有不同的实现方式。
Python提供了threading
模块来支持多线程编程。通过threading
模块,我们可以创建Thread类的实例,并传入一个函数作为线程的执行体。例如:
from threading import Thread
def worker(money):
print("开始干活")
print(f"拿到{money}块工资")
# 通过target指定线程具体执行的工作-传入对应函数,args参数执行传入函数的参数,需要注意这里是元组类型的
thread = Thread(target=worker,args=(100,))
# 启动线程
thread.start()
# 等待线程执行结束
thread.join()
多线程GIL锁的问题
我们来看以下小案例:
import time
def count(n):
while n > 0:
n -=1
start_time = time.time()
count(100000)
end_time = time.time()
print(end_time-start_time)
该程序在我的四核Intel i5 CPU上执行所需的时间为0.003968954086303711秒
把上述的程序改造成多线程模式:
import time
from threading import Thread
def count(n):
while n > 0:
n -=1
start_time = time.time()
# count(100000)
t1 = Thread(target=count, args=[100000 // 2])
t2 = Thread(target=count, args=[100000 // 2])
t1.start()
t2.start()
t1.join()
t2.join()
end_time = time.time()
print(end_time-start_time)
多线程模式下所需要的时间为:0.005413532257080078秒
此程序中使用了2个线程来执行和上面一样的工作,从结果来看效率没有提高反而降低了。其实这里不管使用多少线程,其结果中效率和2个线程效率几乎一样。
为什么会有这种情况?这一切的罪魁祸首来源于GIL:
全局解释器锁(GIL)是Python解释器中的一个机制,用于保护解释器内部数据结构不受多线程并发访问的影响。GIL确保了在任意时刻只有一个线程在解释器中执行Python字节码。这意味着尽管Python中可以创建多个线程,但在任何给定时刻只有一个线程能够真正执行代码,其他线程会被阻塞。
-
当线程在运行时,它会持有GIL锁,其他的进程不能持有GIL锁
-
当线程遇到IO操作时(比如读写磁盘,发送/接收网络数据等)将会释放GIL锁,其他的线程此时可以获取GIL锁
我们可以根据上述知道:即便我们的CPU是多核心,因为Python GIL锁的存在,同一时间只允许执行一个线程,单个时刻也只能使用到1个CPU核心,所以CPU的利用率不高。
多线程适用的场景
在程序运行时候有两种不同类型的计算任务:
CPU密集型计算
CPU密集型计算指的是任务主要耗费CPU资源,而对于其他资源(如内存、磁盘、网络等)的需求相对较少的计算任务。在CPU密集型计算中,程序主要执行大量的计算操作,例如数学运算、图像处理、加密解密、复杂算法等。
IO密集型计算
IO密集型计算指的是任务主要耗费在等待IO操作上,例如磁盘读写、网络通信、数据库查询等。在IO密集型计算中,CPU主要用于处理IO操作的调度和处理。
所以如果是IO密集型计算任务,使用多线程是比较合适的,虽然GIL锁存在导致只能单个线程在CPU内执行,但是在IO处理时不受限制的,IO操作较多时使用多线程依然可以加速任务的执行。
Python多进程
Python中有了多线程,为什么还需要用多进程?
前面我们讲了GIL锁的存在,当多线程遇到了CPU密集型的计算时,反而会降低程序的执行效率
GIL锁同一时间只允许执行一个线程执行,多线程之间还会涉及到上下文切换,上下文切换也需要花费时间
那么什么是多进程呢?
在操作系统中,进程是一个独立运行的程序实例。每个进程都有自己独立的内存空间,一个进程崩溃不会直接影响到其他进程。在多核处理器上,多进程能够实现真正的并行计算,每个进程可以在不同的处理器核心上运行。
Python标准库中的multiprocessing
模块能够实现多进程,这个模块提供了与多线程threading
模块类似的API。使用multiprocessing
,每个Python进程都有自己的Python解释器和内存空间,这样就避开了GIL的限制,能够充分利用多核CPU的计算资源。
from multiprocessing import Process
def worker(money):
print("开始干活")
print(f"拿到{money}块工资")
# 通过target指定进程具体执行的工作-传入对应函数,args参数执行传入函数的参数,需要注意这里是元组类型的
process = Process(target=worker,args=(100,))
# 启动进程
process.start()
# 等待子进程执行结束
process.join()
多进程的主要优势在于能够绕过GIL的限制,利用多核CPU实现真正的并行计算,特别适合处理CPU密集型任务。
其劣势在于运行一个进程消耗的资源比线程多很多,进程间的通讯也是比较复杂(比如需要用到消息队列、管道等机制)
最后总结一句话:对于CPU密集型任务选择多进程,对于IO密集型任务选择多线程。