Python多线程与多进程
自我理解
简单的说:进程就是运行着的程序。
我们写的python程序(或者其他应用程序比如画笔、qq等),运行起来,就称之为一个进程
在windows下面打开任务管理器,里面显示了当前系统上运行着的进程。可以看到,我们系统中有很多的进程运行着,比如qq、搜狗输入法等。这些程序还没有运行的时候,它们的程序代码文件存储在磁盘中,就是那些扩展名为 .exe
文件。双击它们,这些 .exe
文件就被os加载到内存中,运行起来,成为进程
而系统中每个进程里面至少包含一个 线程 。线程是操作系统创建的,每个线程对应一个代码执行的数据结构,保存了代码执行过程中的重要的状态信息。没有线程,操作系统没法管理和维护 代码运行的状态信息。所以没有创建线程之前,操作系统是不会执行我们的代码的。
我们前面写的Python程序,里面虽然没有创建线程的代码,但实际上,当Python解释器程序运行起来(成为一个进程),OS就自动的创建一个线程,通常称为主线程,在这个主线程里面执行代码指令。当解释器执行我们python程序代码的时候。 我们的代码就在这个主线程中解释执行。
现代计算机上面,CPU是多核的, 每个核都可以执行代码。要运行程序里面的代码,操作系统就会分配一个CPU核心去执行该代码。有的时候,我们希望,能够让更多的CPU核心同时执行我们的程序里面的一些代码。
有的时候, 我们有一批任务要执行,而这些任务的执行时间主要耗费在 非CPU计算
上面。比如,我们需要到 前程无忧 网站 抓取 python 开发相关的职位信息。我们要抓取几百个网页的内容, 执行这些抓取信息的任务的代码,时间主要耗费在等待网站返回信息上面。 等待信息返回的时候CPU是空闲的。如果我们像以前那样 在一个线程里面,用一个循环 依次 获取100个网页的信息,就会有很长的时间耗费在 等待服务器返回信息上面。如果我们能用100个线程,同时运行 获取网页信息的代码, 理论上,可以100倍的减少执行时间。
要让多个CPU核心同时去执行任务,我们的程序必须 创建多个线程
,让 CPU 执行 多个线程 对应的代码。
Python代码中创建新线程
那么我们的程序代码怎么产生新线程呢?应用程序必须 通过操作系统提供的 系统调用,请求操作系统分配一个新的线程。python3 将 系统调用创建线程 的功能封装在 标准库 threading 中。
大家来看下面的一段代码
print('主线程执行代码') |
运行该程序,解释器执行到下面代码时
thread = Thread(target=threadFunc, |
创建了一个Thread实例对象,其中,Thread类的初始化参数 有两个.target参数 是指定新线程的 入口函数, 新线程创建后就会 执行该入口函数里面的代码,args 指定了 传给 入口函数threadFunc 的参数。 线程入口函数 参数,必须放在一个元组里面,里面的元素依次作为入口函数的参数。注意,上面的代码只是创建了一个Thread实例对象, 但这时,新的线程还没有创建。要创建线程,必须要调用 Thread 实例对象的 start
方法 。也就是执行完下面代码的时候
thread.start() |
新的线程才创建成功,并开始执行 入口函数threadFunc 里面的代码。有的时候, 一个线程需要等待其它的线程结束,比如需要根据其他线程运行结束后的结果进行处理。这时可以使用 Thread对象的 join
方法
thread.join() |
如果一个线程A的代码调用了 对应线程B的Thread对象的 join
方法,线程A就会停止继续执行代码,等待线程B结束。 线程B结束后,线程A才继续执行后续的代码。所以主线程在执行上面的代码时,就暂停在此处, 一直要等到 新线程执行完毕,退出后,才会继续执行后续的代码。
join通常用于 主线程把任务分配给几个子线程,等待子线程完成工作后,需要对他们任务处理结果进行再处理。就好像一个领导把任务分给几个员工,等几个员工完成工作后,他需要收集他们提交的报告,进行后续处理。这种情况,主线程必须子线程完成才能进行后续操作,所以join就是 等待参数对应的线程完成,才返回。
共享数据的访问控制
做多线程开发,经常遇到这样的情况:多个线程里面的代码 需要访问 同一个 公共的数据对象。这个公共的数据对象可以是任何类型, 比如一个 列表、字典、或者自定义类的对象。有的时候,程序 需要 防止线程的代码 同时操作 公共数据对象。 否则,就有可能导致 数据的访问互相冲突影响。
这时,可以使用 threading库里面的锁对象 Lock 去保护。
from threading import Thread,Lock |
Lock 对象的acquire方法 是申请锁。
每个线程在 操作共享数据对象之前,都应该 申请获取操作权,也就是 调用该 共享数据对象对应的锁对象的acquire方法。
如果线程A 执行如下代码,调用acquire方法的时候,bankLock.acquire() 别的线程B 已经申请到了这个锁, 并且还没有释放,那么 线程A的代码就在此处 等待 线程B 释放锁,不去执行后面的代码。
直到线程B 执行了锁的 release 方法释放了这个锁, 线程A 才可以获取这个锁,就可以执行下面的代码了。
如果这时线程B 又执行 这个锁的acquire方法, 就需要等待线程A 执行该锁对象的release方法释放锁, 否则也会等待,不去执行后面的代码。
daemon线程
大家执行下面的代码
from threading import Thread |
可以发现,主线程先结束,要过个2秒钟,等子线程运行完,整个程序才会结束退出。
因为: Python程序中当所有的 非daemon线程
结束了,整个程序才会结束
主线程是非daemon线程,启动的子线程缺省也是 非daemon 线程。所以,要等到 主线程和子线程 都结束,程序才会结束。
我们可以在创建线程的时候,设置daemon参数值为True,如下
from threading import Thread |
再次运行,可以发现,只要主线程结束了,整个程序就结束了。因为只有主线程是非daemon线程。
多进程
Python 官方解释器 的每个线程要获得执行权限,必须获取一个叫 GIL (全局解释器锁) 的东西。
这就导致了 Python 的多个线程 其实 并不能同时使用 多个CPU核心。
所以如果是计算密集型的任务,不能采用多线程的方式。
如果需要利用电脑多个CPU核心的运算能力,可以使用Python的多进程库,如下
from multiprocessing import Process |
运行后,打开任务管理器,可以发现 有3个Python进程,其中主进程CPU占用率为0,两个子进程CPU各占满了一个核心的运算能力。
仔细看上面的代码,可以发现和多线程的使用方式非常类似。
还有一个问题,主进程如何获取 子进程的 运算结果呢?
可以使用多进程库 里面的 Manage 对象,如下
from multiprocessing import Process,Manager |
菜鸟教程
多线程类似于同时执行多个不同程序,多线程运行有如下优点:
- 使用线程可以把占据长时间的程序中的任务放到后台去处理。
- 用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
- 程序的运行速度可能加快。
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
- 线程可以被抢占(中断)。
- 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) – 这就是线程的退让。
线程可以分为:
内核线程:由操作系统内核创建和撤销。
用户线程:不需要内核支持而在用户程序中实现的线程。
Python3 线程中常用的两个模块为:
- _thread
- threading(推荐使用)
thread 模块已被废弃。用户可以使用 threading 模块代替。所以,在 Python3 中不能再使用”thread” 模块。为了兼容性,Python3 将 thread 重命名为 “_thread”。
开始学习Python线程
Python中使用线程有两种方式:函数或者用类来包装线程对象。
函数式:调用 _thread 模块中的start_new_thread()函数来产生新线程。语法如下:
_thread.start_new_thread ( function, args[, kwargs] ) |
参数说明:
- function - 线程函数。
- args - 传递给线程函数的参数,他必须是个tuple类型。
- kwargs - 可选参数。
#!/usr/bin/python3 |
线程模块
Python3 通过两个标准库 _thread 和 threading 提供对线程的支持。
_thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。
threading 模块除了包含 _thread 模块中的所有方法外,还提供的其他方法:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
run(): 用以表示线程活动的方法。
start():
启动线程活动。
join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。
使用 threading 模块创建线程
我们可以通过直接从 threading.Thread 继承创建一个新的子类,并实例化后调用 start() 方法启动新线程,即它调用了线程的 run() 方法:
#!/usr/bin/python3 |
线程同步
如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。
使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release 方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间。如下:
多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。
考虑这样一种情况:一个列表里所有元素都是0,线程”set”从后向前把所有元素改成1,而线程”print”负责从前往后读取列表并打印。
那么,可能线程”set”开始改的时候,线程”print”便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。
锁有两种状态——锁定和未锁定。每当一个线程比如”set”要访问共享数据时,必须先获得锁定;如果已经有别的线程比如”print”获得锁定了,那么就让线程”set”暂停,也就是同步阻塞;等到线程”print”访问完毕,释放锁以后,再让线程”set”继续。
经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。
#!/usr/bin/python3 |
线程优先级队列( Queue)
Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue。
这些队列都实现了锁原语,能够在多线程中直接使用,可以使用队列来实现线程间的同步。
Queue 模块中的常用方法:
- Queue.qsize() 返回队列的大小
- Queue.empty() 如果队列为空,返回True,反之False
- Queue.full() 如果队列满了,返回True,反之False
- Queue.full 与 maxsize 大小对应
- Queue.get([block[, timeout]])获取队列,timeout等待时间
- Queue.get_nowait() 相当Queue.get(False)
- Queue.put(item) 写入队列,timeout等待时间
- Queue.put_nowait(item) 相当Queue.put(item, False)
- Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
- Queue.join() 实际上意味着等到队列为空,再执行别的操作
#!/usr/bin/python3 |