Python多线程与多进程深度解析:GIL机制、性能对比及适用场景
本文深入解析Python中多线程与多进程的实现机制,重点剖析GIL(全局解释器锁)对CPU密集型任务的影响。通过对比实验展示线程与进程在运算耗时上的显著差异,明确多线程适用于IO密集型场景,而多进程更适合CPU密集型任务,为开发者提供清晰的并发编程选型依据。
前言与概述
前言
在技术学习中,理解核心概念与原理至关重要,细节往往可以通过实践逐步完善。掌握的关键在于深入理解,借助具体实例和实际操作来感性体会概念,能显著提升学习效果。本文将通过具体案例,简要介绍Python中多线程与多进程的实现方式,后续还将探讨进程间与线程间的通信机制。
Python多线程基础
Python多线程
Python标准库提供了thread和threading两个模块以支持线程操作。在Python 3中,前者已不再受支持,后者作为更高层级的封装库成为主流选择。下文将以threading模块为例进行阐述。
创建线程
在Python中实现线程主要有两种途径:
1. 实例化一个threading.Thread对象,并将一个初始化函数(initial function)作为线程执行的入口传入;
2. 继承threading.Thread类,并重写其run方法;
方式1:创建threading.Thread对象
import threading
import time
def tstart(arg):
time.sleep(0.5)
print("%s running...." % arg)
if __name__ == '__main__':
t1 = threading.Thread(target=tstart, args=('This is thread 1',))
t2 = threading.Thread(target=tstart, args=('This is thread 2',))
t1.start()
t2.start()
print("This is main function")
运行结果如下:
This is main function
This is thread 2 running....
This is thread 1 running....
方式2:继承threading.Thread并重写run方法
import threading
import time
class CustomThread(threading.Thread):
def __init__(self, thread_name):
# step 1: call base __init__ function
super(CustomThread, self).__init__(name=thread_name)
self._tname = thread_name
def run(self):
# step 2: overide run function
time.sleep(0.5)
print("This is %s running...." % self._tname)
if __name__ == "__main__":
t1 = CustomThread("thread 1")
t2 = CustomThread("thread 2")
t1.start()
t2.start()
print("This is main function")
其执行结果与方式1相同。
threading.Thread
上述两种方法在本质上都是直接或间接地使用了threading.Thread类。
threading.Thread(group=None, target=None, name=None, args=(), kwargs={})
将上述两种创建线程的方式关联起来看:
import threading
import time
class CustomThread(threading.Thread):
def __init__(self, thread_name, target = None):
# step 1: call base __init__ function
super(CustomThread, self).__init__(name=thread_name, target=target, args = (thread_name,))
self._tname = thread_name
def run(self):
# step 2: overide run function
# time.sleep(0.5)
# print("This is %s running....@run" % self._tname)
super(CustomThread, self).run()
def target(arg):
time.sleep(0.5)
print("This is %s running....@target" % arg)
if __name__ == "__main__":
t1 = CustomThread("thread 1", target)
t2 = CustomThread("thread 2", target)
t1.start()
t2.start()
print("This is main function")
运行结果如下:
This is main function
This is thread 1 running....@target
This is thread 2 running....@target
上述代码说明了以下几点:
1. 无论采用哪种方式创建线程,指定的参数最终都会传递给threading.Thread类;
2. 传递给线程的目标函数是在基类Thread的run方法中被调用的,除非run方法被重写。
关于threading模块的更多属性和方法可参考官方文档,此处重点介绍threading.Thread对象的相关方法。
以下是threading.Thread提供的线程对象方法和属性:
- start():创建线程后调用start启动线程,此时线程进入等待CPU调度状态,为执行run函数做准备;
- run():线程执行的入口函数,其内部会调用用户指定的target函数,或执行被重写的run函数;
- join([timeout]):阻塞调用该方法的线程,直到被调用的线程执行完毕或超时。通常在主线程中调用此方法,以等待其他子线程执行完成;
- name、getName()&setName():用于获取或设置线程名称;
- ident:整数类型的线程标识符,在调用start之前为None;
- isAlive()、is_alive():在start执行之后到run执行完毕之前返回True;
- daemon、isDaemon()&setDaemon():用于设置或获取守护线程状态;
以上方法主要用于创建线程后的管理和信息获取。
多线程执行与同步
多线程执行
在主线程中创建多个子线程后,它们之间默认没有协作和同步机制。除主线程外,每个子线程均从run方法开始执行,直至执行完毕。
join
我们可以利用join方法使主线程阻塞,从而等待其创建的子线程执行完成。
import threading
import time
def tstart(arg):
print("%s running....at: %s" % (arg,time.time()))
time.sleep(1)
print("%s is finished! at: %s" % (arg,time.time()))
if __name__ == '__main__':
t1 = threading.Thread(target=tstart, args=('This is thread 1',))
t1.start()
t1.join() # 当前线程阻塞,等待t1线程执行完成
print("This is main function at:%s" % time.time())
运行结果如下:
This is thread 1 running....at: 1564906617.43
This is thread 1 is finished! at: 1564906618.43
This is main function at:1564906618.43
若不加任何限制,当主线程执行完毕后,当前程序并不会立即结束,必须等待所有子线程结束后,进程才会终止。
若将上述程序中的t1.join()移除,执行结果如下:
This is thread 1 running....at: 1564906769.52
This is main function at:1564906769.52
This is thread 1 is finished! at: 1564906770.52
可以通过将创建的线程指定为守护线程(daemon),这样当主线程执行完毕后,会立即终止未执行完的子线程,进而结束整个程序。
deamon守护线程
import threading
import time
def tstart(arg):
print("%s running....at: %s" % (arg,time.time()))
time.sleep(1)
print("%s is finished! at: %s" % (arg,time.time()))
if __name__ == '__main__':
t1 = threading.Thread(target=tstart, args=('This is thread 1',))
t1.setDaemon(True)
t1.start()
# t1.join() # 当前线程阻塞,等待t1线程执行完成
print("This is main function at:%s" % time.time())
运行结果如下:
This is thread 1 running....at: 1564906847.85
This is main function at:1564906847.85
Python多进程实现
Python多进程
与用于创建多线程的threading模块不同,Python提供了multiprocessing模块来创建多进程。首先查看创建进程的两种方式。
The multiprocessing package mostly replicates the API of the threading module. —— python doc
创建进程
创建进程的方式与创建线程类似:
1. 实例化一个multiprocessing.Process对象,并传入一个初始化函数(initial function)作为新进程的执行入口;
2. 继承multiprocessing.Process,并重写run方法;
方式1:
from multiprocessing import Process
import os, time
def pstart(name):
# time.sleep(0.1)
print("Process name: %s, pid: %s "%(name, os.getpid()))
if __name__ == "__main__":
subproc = Process(target=pstart, args=('subprocess',))
subproc.start()
subproc.join()
print("subprocess pid: %s"%subproc.pid)
print("current process pid: %s" % os.getpid())
运行结果如下:
Process name: subprocess, pid: 4888
subprocess pid: 4888
current process pid: 9912
方式2:
from multiprocessing import Process
import os, time
class CustomProcess(Process):
def __init__(self, p_name, target=None):
# step 1: call base __init__ function()
super(CustomProcess, self).__init__(name=p_name, target=target, args=(p_name,))
def run(self):
# step 2:
# time.sleep(0.1)
print("Custom Process name: %s, pid: %s "%(self.name, os.getpid()))
if __name__ == '__main__':
p1 = CustomProcess("process_1")
p1.start()
p1.join()
print("subprocess pid: %s"%p1.pid)
print("current process pid: %s" % os.getpid())
此处值得思考:如果像多线程那样存在一个全局变量share_data,不同进程同时访问该变量会有问题吗?
由于每个进程拥有独立的内存地址空间且相互隔离,因此不同进程看到的share_data是不同的,分别位于各自的地址空间中,同时访问不会引发冲突。这一点需要特别注意。
Subprocess模块
既然提到多进程,顺便介绍另一种创建进程的方式。
Python提供了Subprocess模块,允许在程序执行过程中调用外部程序。
例如,我们可以在Python程序中打开记事本、cmd,或在特定时间关机:
>>> import subprocess
>>> subprocess.Popen(['cmd'])
<subprocess.Popen object at 0x0339F550>
>>> subprocess.Popen(['notepad'])
<subprocess.Popen object at 0x03262B70>
>>> subprocess.Popen(['shutdown', '-p'])
或者使用ping测试网络连通性:
>>> res = subprocess.Popen(['ping', 'www.cnblogs.com'], stdout=subprocess.PIPE).communicate()[0]
>>> print res
正在 Ping www.cnblogs.com [101.37.113.127] 具有 32 字节的数据:
来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91 来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91
来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91
来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91
101.37.113.127 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
最短 = 1ms,最长 = 1ms,平均 = 1ms
线程与进程性能对比
Python多线程与多进程比较
首先来看两个例子:
开启两个Python线程分别执行一亿次加一操作,以及单独使用一个线程执行相同操作:
def tstart(arg):
var = 0
for i in xrange(100000000):
var += 1
if __name__ == '__main__':
t1 = threading.Thread(target=tstart, args=('This is thread 1',))
t2 = threading.Thread(target=tstart, args=('This is thread 2',))
start_time = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
print("Two thread cost time: %s" % (time.time() - start_time))
start_time = time.time()
tstart("This is thread 0")
print("Main thread cost time: %s" % (time.time() - start_time))
运行结果如下:
Two thread cost time: 20.6570000648
Main thread cost time: 2.52800011635
上述例子中,如果只开启t1和t2中的一个线程,其运行时间与主线程基本一致。这将在后文解释原因。
使用两个进程执行上述操作:
def pstart(arg):
var = 0
for i in xrange(100000000):
var += 1
if __name__ == '__main__':
p1 = Process(target = pstart, args = ("1", ))
p2 = Process(target = pstart, args = ("2", ))
start_time = time.time()
p1.start()
p2.start()
p1.join()
p2.join()
print("Two process cost time: %s" % (time.time() - start_time))
start_time = time.time()
pstart("0")
print("Current process cost time: %s" % (time.time() - start_time))
运行结果如下:
Two process cost time: 2.91599988937
Current process cost time: 2.52400016785
对比分析
双进程并行执行与单进程执行相同的运算代码,耗时基本相同,双进程耗时略多,这可能是由于进程创建和销毁涉及系统调用,带来了额外的时间开销。
然而对于Python线程,双线程并行执行的耗时远高于单线程,效率相差近10倍。若将两个并行线程改为串行执行,即:
t1.start()
t1.join()
t2.start()
t2.join()
#Two thread cost time: 5.12199997902
#Main thread cost time: 2.54200005531
可以看到,三个线程串行执行时,每个线程的执行时间基本相同。
其本质原因在于双线程是并发执行,而非真正的并行执行。这归因于GIL锁。
GIL锁机制解析
GIL锁
讨论Python多线程时,不得不提GIL(Global Interpreter Lock,全局解释器锁)。这是目前主流的Python解释器CPython为保证数据安全而实现的一种锁机制。无论进程中有多少线程,只有获取GIL锁的线程才能在CPU上运行,即使是在多核处理器上。对于单个进程而言,无论有多少线程,任一时刻只有一个线程在执行。因此,对于CPU密集型线程,其效率不仅不高,反而可能更低。Python多线程更适用于IO密集型程序。对于确实需要并行运行的程序,建议考虑多进程。
线程对锁的争夺、CPU对线程的调度以及线程间的切换均会产生时间开销。
线程与进程的核心区别
线程与进程区别
以下简要比较线程与进程:
- 进程是资源分配的基本单位,线程是CPU执行和调度的基本单位;
- 通信/同步方式:
- 进程:
- 通信方式:管道,FIFO,消息队列,信号,共享内存,socket,stream流;
- 同步方式:PV信号量,管程
- 线程:
- 同步方式:互斥锁,递归锁,条件变量,信号量
- 通信方式:位于同一进程的线程共享进程资源,因此线程间没有类似于进程间用于数据传递的通信方式,线程间的通信主要用于线程同步。
- 进程:
- CPU上真正执行的是线程,线程比进程轻量,其切换和调度代价比进程小;
- 线程间对于共享的进程数据需考虑线程安全问题。由于进程之间是隔离的,拥有独立的内存空间资源,相对更安全,只能通过上述列出的IPC(Inter-Process Communication)进行数据传输;
- 系统由一个个进程组成,每个进程包含代码段、数据段、堆空间和栈空间,以及操作系统共享部分,具有等待、就绪和运行三种状态;
- 一个进程可以包含多个线程,线程之间共享进程的资源(文件描述符、全局变量、堆空间等),寄存器变量和栈空间等是线程私有的;
- 操作系统中一个进程挂掉不会影响其他进程。如果一个进程中的某个线程挂掉且OS对线程的支持是多对一模型,则会导致当前进程挂掉;
- 如果CPU和系统支持多线程与多进程,多个进程并行执行的同时,每个进程中的线程也可以并行执行,这样才能最大限度地发挥硬件性能;
上下文切换与选型建议
线程和进程的上下文切换
进程切换涉及众多因素,包括将寄存器内容保存到任务状态段TSS、切换页表、堆栈等。简单来说可分为两步:
页全局目录切换,使CPU到新进程的线性地址空间寻址; 切换内核态堆栈和硬件上下文,硬件上下文包含CPU寄存器的内容,存放在TSS中; 线程运行于进程地址空间,切换过程不涉及空间变换,仅涉及第二步;
使用多线程还是多进程?
CPU密集型:程序需要占用CPU进行大量的运算和数据处理;
I/O密集型:程序中需要频繁进行I/O操作,例如网络中socket数据传输和读取等;
由于Python多线程并非并行执行,因此更适合I/O密集型程序;多进程并行执行则适用于CPU密集型程序;
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









