Python3快速入门(九)——Python3并发编程
一、Python线程模块
1、线程简介
一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程本身不拥有系统资源,与进程内的其它线程共享进程的所有资源。一个进程中至少有一个线程,并作为程序的入口,即主线程,其它线程称为工作线程。
多线程,是指从软件或者硬件上实现多个线程并发执行的技术。支持多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,进而提升整体处理性能。
2、线程状态
线程有就绪、阻塞、运行三种基本状态。就绪状态是指线程具备运行的所有条件,在等待CPU执行; 运行状态是指线程占有CPU正在运行; 阻塞状态是指线程在等待一个事件,逻辑上不可执行。
三种状态的相互转化如下图所示:
2、threading线程模块
Python3 通过_thread 和 threading两个模块提供对线程的支持。
_thread提供了低级别的、原始的线程以及简单锁,相比于 threading 模块的功能比较有限,是对已经废弃的thread模块的兼容性支持方案。
threading 模块除了包含_thread模块中的所有方法外,还提供的如下方法:
threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount(): 返回正在运行线程的数量,与len(threading.enumerate())有相同的结果。
Thread类提供方法如下:
run(): 用以表示线程活动的方法。
start():启动线程活动。
join([time]): 等待至线程中止。阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。
3、multiprocessing模块
multiprocessing模块是跨平台版本的多进程模块,提供了一个Process类代表一个进程对象。创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例。
Process(self,group=None,target=None,name=None,args=(),kwargs=())
group参数未使用,值始终为None。
target表示调用的对象,子进程要执行的任务。
name可以为子进程命名。
args指定传结target函数的位置参数,是一个元组形式,必须有逗号,如:args=(‘monicx’,)
kwargs指定传结target函数的关键字参数,是一个字典,如kwargs={‘name’:‘monicx’,‘age’:18}
Process方法如下:
start():启动进程,并调用子进程的run()方法。
run():进程启动进运行的方法,在run内调用target指定的函数,子进程类中一定要实现run方法。
terminate():强制终止进程,不会进行任何清理操作,如果进程创建了子进程,子进程会变成僵尸进程;如果进程还保存了一个锁,则不会释放进程锁,进而导致死锁。
is_alive():判断进程是否是“活着”的状态。
join(timeout):让主进程程等待某一子进程结束,才继续执行主进程。timeout是可选的超时时间,超过一个时间主进程就不等待。
4、全局解释锁GIL
Python并不支持真正意义上的多线程。Python中提供了多线程模块,但如果想通过多线程提高代码的速度,并不推荐使用多线程模块。Python中有一个全局锁Global Interpreter Lock(GIL),全局锁会确保任何时候多个线程中只有一个会被执行。线程的执行速度非常快,会误以为线程是并行执行的,但实际上都是轮流执行。经过GIL处理后,会增加线程执行的开销。
全局锁 GIL(Global interpreter lock) 并不是 Python 的特性,而是在实现 Python 解析器(CPython)时所引入的一个概念。Python有CPython,PyPy,Psyco 等不同的 Python 执行环境,其中 JPython 没有GIL。CPython 是大部分环境下默认的 Python 执行环境,GIL 并不是 Python 的特性,Python 完全可以不依赖于 GIL。
GIL 限制了同一时刻只能有一个线程运行,无法发挥多核 CPU 的优势。GIL 本质是互斥锁,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。在一个 Python 的进程内,不仅有主线程或者由主线程开启的其它线程,还有解释器开启的垃圾回收等解释器级别的线程。进程内,所有数据都是共享的,代码作为一种数据也会被所有线程共享,多个线程先访问到解释器的代码,即拿到执行权限,然后将 target 的代码交给解释器的代码去执行,解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,因此为了保证数据安全需要加锁处理,即 GIL。
由于GIL 的存在,同一时刻同一进程中只有一个线程被执行。多核 CPU可以并行完成计算,因此多核可以提升计算性能,但 CPU 一旦遇到 I/O 阻塞,仍然需要等待,所以多核CPU对 I/O 密集型任务提升不明显。根据执行任务是计算密集型还是I/O 密集型,不同场景使用不同的方法,对于计算密集型任务,多进程占优势,对于 I/O 密集型任务,多线程占优势。
计算密集型任务-多进程方案:
# -*- coding:utf-8 -*-
from multiprocessing import Process
import os
import time
def work():
result = 0
for x in range(100000000):
result *= x
if __name__ == "__main__":
processes = []
print("CPU: ", os.cpu_count())
start = time.time()
for i in range(4):
p = Process(target=work)
processes.append(p)
p.start()
for p in processes:
p.join()
end = time.time()
print("计算密集型任务,多进程耗时 %s" % (end - start))
# output:
# CPU: 4
# 计算密集型任务,多进程耗时 9.485123872756958
计算密集型任务-多线程方案:
# -*- coding:utf-8 -*-
from threading import Thread
import os, time
def work():
res = 0
for x in range(100000000):
res *= x
if __name__ == "__main__":
threads = []
print("CPU: ",os.cpu_count())
start = time.time()
for i in range(4):
thread = Thread(target=work) # 多进程
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end = time.time()
print("计算密集型任务,多线程耗时 %s" % (end - start))
# output:
# CPU: 4
# 计算密集型任务,多线程耗时 18.434288501739502
IO密集型任务-多进程方案:
# -*- coding:utf-8 -*-
from multiprocessing import Process
import os, time
def work():
time.sleep(2)
print("hello,Python----------------------------------------------------", file=open("tmp.txt", "w"))
if __name__ == "__main__":
processes = []
print("CPU: ", os.cpu_count())
start = time.time()
for i in range(400):
p = Process(target=work) # 多进程
processes.append(p)
p.start()
for p in processes:
p.join()
stop = time.time()
print("I/0密集型任务,多进程耗时 %s" % (stop - start))
# output:
# CPU: 4
# I/0密集型任务,多进程耗时 2.8894519805908203
IO密集型任务-多线程方案:
# -*- coding:utf-8 -*-
from threading import Thread
import os, time
def work():
time.sleep(2)
print("hello,Python----------------------------------------------------", file=open("tmp.txt", "w"))
if __name__ == "__main__":
threads = []
print("CPU: ", os.cpu_count())
start = time.time()
for x in range(400):
thread = Thread(target=work)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end = time.time()
print("IO密集型任务,多线程耗时 %s" % (end - start))
# output:
# CPU: 4
# IO密集型任务,多线程耗时 2.044438362121582
二、创建线程
1、threading.Thread实例化
threading.Thread构造函数如下:
def __init__(self, group=None, target=None, name=None,
args=(), kwargs=None, *, daemon=None):
创建 threading.Thread 实例,调用其 start() 方法。
# -*- coding:utf-8 -*-
import time
import threading
def work_task(counter):
print("%s %s" % (threading.current_thread().name, time.ctime(time.time())))
n = counter;
while n > 0:
time.sleep(1)
n -= 1
if __name__ == "__main__":
print("main thread start:", time.strftime("%Y-%m-%d %H:%M:%S"))
threads = []
for x in range(10):
thread = threading.Thread(target=work_task, args=(x, ))
threads.append(thread)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print("main thread end:", time.strftime("%Y-%m-%d %H:%M:%S"))
# output:
# main thread start: 2019-07-03 21:49:58
# Thread-1 Wed Jul 3 21:49:58 2019
# Thread-2 Wed Jul 3 21:49:58 2019
# Thread-3 Wed Jul 3 21:49:58 2019
# Thread-4 Wed Jul 3 21:49:58 2019
# Thread-5 Wed Jul 3 21:49:58 2019
# Thread-6 Wed Jul 3 21:49:58 2019
# Thread-7 Wed Jul 3 21:49:58 2019
# Thread-8 Wed Jul 3 21:49:58 2019
# Thread-9 Wed Jul 3 21:49:58 2019
# Thread-10 Wed Jul 3 21:49:58 2019
# main thread end: 2019-07-03 21:50:07
2、threading.Thread子线程
可以通过直接从 threading.Thread类继承创建一个新的子类,在子类中重写 run() 和 init() 方法,实例化后调用 start() 方法启动新线程,start函数内部会调用线程的 run() 方法。
# -*- coding:utf-8 -*-
import threading
import time
class WorkThread(threading.Thread):
def __init__(self, thread_id, name):
threading.Thre