本文转载自:https://blog.youkuaiyun.com/xw1680/article/details/104442017
Python进程和线程
为了实现在同一时间运行多个任务,Python引入了多线程的概念。在Python中可以通过方便、快捷的方式启动多线程模式。多线程常被应用在符合并发机制的程序中,例如网络程序等。为了再进一步将工作任务细分,在一个进程内可以使用多个线程。
1.多任务的概念
在了解进程之前,我们需要知道多任务的概念。多任务,顾名思义,就是指操作系统能够执行多个任务。例如,使用Windows或Linux或Mac等操作系统可以同时看电影、聊天、查看网页等,此时,操作系统就是在执行多任务。
1.1 多任务的执行方式
1.1.1 并发
在一段时间内交替去执行任务。例如:对于单核cpu处理多任务,操作系统轮流让各个软件交替执行,假如: 软件1执行0.01秒,切换到软件2,软件2执行0.01秒,再切换到软件3,执行0.01秒……这样反复执行下去。表面上看,每个软件都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像这些软件都在同时执行一样,这里需要注意单核cpu是并发的执行多任务的。
1.1.2 并行
对于多核cpu处理多任务,操作系统会给cpu的每个内核安排一个执行的软件,多个内核是真正的一起执行软件。这里需要注意多核cpu是并行的执行多任务,始终有多个软件一起执行。
1.2 总结
简单理解:任务数大于CPU核数为并发,任务数小于CPU核数为并行
使用多任务就能充分利用CPU资源,提高程序的执行效率,让你的程序具备处理多个任务的能力
多任务执行方式有两种方式:并发和并行,这里并行才是多个任务真正意义一起执行
2. 什么是进程
上述中说到的每个任务就是 一个进程。 我们可以打开Windows的任务管理器,查看一下操作系统正在执行的进程,如图所示。图中显示的进程不仅包括应用程序(如pycharm、Wechat、谷歌浏览器等),还包括系统进程。
进程(process) 是计算机中已运行程序的实体。进程与程序不同,程序本身只是指令、数据及其组织形式的描述,进程才是程序(指令和数据)的真正运行实例。例如,在没有打开QQ时,QQ只是程序。打开QQ后,操作系统就为QQ开启了一个进程。再打开一个QQ音乐,则又开启了一个进程。如图所示:
3. 创建进程的常用方式
在Python中有多个模块可以创建进程,比较常用的有os .fork()函数、multiprocessing包和Pool进程池。由于os .fork()函数只适用于Unix/Linux/Mac系统上运行,在Windows操作系统中不可用,所以这里我重点介绍multiprocessing包和Pool进程池这2个跨平台模块。
3.1 使用multiprocessing创建进程
multiprocessing提供了一 个Process 类来代表一个进程对象,语法如下:
Process([group [,target [,name [, args [, kwargl]]]])
Process类的参数说明如下:
- group: 表示进程组,目前只能设置为None,一般不需要进行设置。
- target:表示当前进程启动时执行的可调用对象。即进程执行的目标任务,一般是函数名或者是方法名
- name : 为当前进程实例的别名。如果不设置,默认为Process-1,Process-2,…Process-N
- args: 表示传递给target函数的参数元组。
- kwargs: 表示传递给target函数的参数字典。
例1,实例化Process类,执行子进程,代码如下
import multiprocessing # 1.导入进程包
import time
# 跳舞任务
def dance():
for i in range(3):
print("跳舞中...")
time.sleep(0.2)
# 唱歌任务
def sing():
for i in range(3):
print("唱歌中...")
time.sleep(0.2)
# 2.创建子进程
# (自己手动创建的进程称为子进程,在init.py中已经
# 导入Process类)
if __name__ == "__main__":
# 大家在写代码的时候,最好是加上判断是否是主模块
#(简单理解:你要执行哪个py文件,哪个py文件就叫做主模块) 原因:
# 1.防止别人导入的时候,执行相关的代码
# 2.在创建进程的时候,在Windows系统中会拷贝主进程中的
# 所有代码,所以会造成递归创建子进程,形成死递归,报错。
dance_process = multiprocessing.Process(target=dance)
sing_process = multiprocessing.Process(target=sing)
# 3.启动进程执行对应的任务
dance_process.start()
sing_process.start()
# 注意: 进程执行是无序的,具体哪个先执行是由操作系统调度所决定的
上述代码中,先实例化Process类,然后使用dance_process.start()/sing_process.start()方法启动子进程,开始执行dance()/sing()函数。Process的实例常用的方法除start()外,还有如下常用方法:
is alive0): 判断进程实例是否还在执行。
join(timecout):是否等待进程实例执行结束,或等待多少秒。.
start(): 启动进程实例(创建子进程)。
run(): 如果没有给定target参数,对这个对象调用start()方法时,就将执行对象中的run()方法。
terminate(): 不管任务是否完成,立即终止。
Process类还有如下常用属性:
name:当前进程实例别名,默认为Process一N, N为从1开始递增的整数。
pid:当前进程实例的PID值。
下面通过一个简单示例演示Process类的方法和属性的使用,创建2个子进程,使用os模块输出父进程和子进程的ID,并调用Process类的name和pid属性,代码如下:
import multiprocessing # 1.导入进程包
import time
import os
# 跳舞任务
def dance():
# 获取当前进程(子进程)的编号
dance_process_id = os.getpid()
# 获取当前进程对象,查看当前代码是由哪个进程对象执行的:multiprocessing.currentprocess()
print("danceprocess_id", dance_process_id, multiprocessing.current_process())
# 获取当前进程的父进程编号
dance_process_parent_id = os.getppid()
print(f"dance_process的父进程编号是: {dance_process_parent_id}")
for i in range(3):
print("跳舞中...")
time.sleep(0.2)
# 拓展: 根据进程编号强制杀死指定进程
# os.kill(dance_process_id, 9)
# 唱歌任务
def sing():
# 获取当前进程(子进程)的编号
sing_process_id = os.getpid()
# 获取当前进程对象,查看当前代码是由哪个进程对象执行的:multiprocessing.current_process()
print("sing_process_id", sing_process_id, multiprocessing.current_process())
# 获取当前进程的父进程编号
sing_process_parent_id = os.getppid()
print(f"sing_process的父进程编号是: {sing_process_parent_id}")
for i in range(3):
print("唱歌中...")
time.sleep(0.2)
if __name__ == '__main__':
# 获取当前进程(主进程)的编号
main_process_id = os.getpid()
# 获取当前进程对象,查看当前代码是由哪个进程对象执行的: multiprocessing.current_process()
print("main_process_id", main_process_id, multiprocessing.current_process())
# 2.创建子进程对象
dance_process = multiprocessing.Process(target=dance, name="dance")
sing_process = multiprocessing.Process(target=sing)
# 3.启动进程执行对应的任务
dance_process.start()
sing_process.start()
# 4.控制台输出子进程的名字以编号
print("dance_process_name:", dance_process.name, "dance_process.pid:", dance_process.pid)
print("sing_process_name:", sing_process.name, "sing_process.pid:", sing_process.pid)
# 5.判断子进程是否还在执行,如果在执行则返回True
print(dance_process.is_alive())
print(sing_process.is_alive())
dance_process.join() # 主进程等待子进程dance_process结束
sing_process.join() # 主进程等待子进程sing_process结束
print("--------主进程结束---------")
# 注意: 进程执行是无序的,具体哪个先执行是由操作系统调度所决定的
程序运行结果如下:
下面通过一个简单示例演示进程执行带有参数的任务
import multiprocessing
# 显示信息的任务
def show_info(name, age):
print(name, age)
if __name__ == '__main__':
# 创建子进程
# 以元组方式传参,元组里面的元素顺序要和函数的参数顺序保持一致
sub_process = multiprocessing.Process(target=show_info, args=("guiyin", 18))
# 启动进程执行对应的任务
sub_process.start()
# # 以字典方式传参,字典里面的key要和函数里面的参数名保持一致,没有顺序要求
# sub_process = multiprocessing.Process(target=show_info, kwargs={"age": 18, "name": "guiyin"})
#
# # 启动进程
# sub_process.start()
3.2 使用Process子类创建进程
对于一些简单的小任务,通常使用Process(target=xxx)的方式实现多进程。但是如果要处理复杂任务的进程,通常定义一个类,使其继承Process类,每次实例化这个类的时候,就等同于实例化一个进程对象。下面,通过一个示例来学习一下如何通过使用Process子类创建多个进程。
使用Process 子类方式创建2个子进程,分别输出父、子进程的PID,以及每个子进程的状态和运行时间,代码如下:
# import multiprocessing
from multiprocessing import Process
import time
import os
# 定义类 继承Process类
class SubProcess(Process):
# 由于Process类本身也有init初始化方法,这个子类相当于重写了父类的这个方法
def __init__(self, interval, name=""):
# Process.init(self) # 调用Process父类的初始化方法
super().__init__() # 使用super重写
self.interval = interval # 接收参数interval
if name: # 判断传递的参数name是否存在
self.name = name # 如果传递参数name ,则为子进程创建name属性,否则使用默认属性
# 重写了Process类的run()方法
def run(self):
print("子进程(%s)开始执行,父进程为(%s) " % (os.getpid(), os.getppid()))
t_start = time.time()
time.sleep(self.interval)
t_stop = time.time()
print("子进程(%s )执行结束,耗时%.2f秒" % (os.getpid(), t_stop - t_start))
if __name__ == "__main__":
print("一一一父进程开始执行一一一")
print("父进程PID: %s" % os.getpid())
# 输出当前程序的ID
p1 = SubProcess(interval=1, name="Amo")
p2 = SubProcess(interval=2)
# 对一个不包含target属性的Process类执行start()方法,就会运行这个类中的run()方法
# 所以这里会执行p1.run()
p1.start()
# 启动进程p1
p2.start()
# 启动进程p2
# 输出p1和p2进程的执行状态,如果真正进行,返回True;否则返回False
print("p1.is alive=%s" % p1.is_alive())
print("p2.is alive=%s" % p2.is_alive())
# 输出p1和p2进程的别名和PID
print("p1.name=%s" % p1.name)
print("p1.pid=%s" % p1.pid)
print("p2.name=%s" % p2.name)
print("p2. pid=%s" % p2.pid)
print("一一一等待子进程一一一")
p1.join()
# 等待p1进程结束
p2.join()
# 等待p2进程结束
print("一一一父进程执行结束一一一一")
3.3 使用进程池Pool创建进程
在上述中,我们使用Process类创建了2个进程。如果要创建几十个或者上百个进程,则需要实例化更多个Process类。有没有更好的创建进程的方式解决这类问题呢?答案就是使用multiprocessing模块提供的Pool类,即Pool进程池。为了更好的理解进程池,可以将进程池比作水池,如图所示。我们需要完成放满10个水盆的水的任务,而在这个水池中,最多可以安放3个水盆接水,也就是同时可以执行3个任务,即开启3个进程。为更快完成任务,现在打开3个水龙头开始放水,当有一个水盆的水接满时,即该进程完成1个任务,我们就将这个水盆的水倒入水桶中,然后继续接水,即执行下一个任务。如果3个水盆每次同时装满水,那么在放满第9盆水后,系统会随机分配1个水盆接水,另外2个水盆空闲。简单理解就是说:重复的进行了进程资源利用,不用频繁地创建进程,消耗系统资源。
接下来,先来了解一下Pool类的常用方法。常用方法及说明如下:
apply async(func[, args[, kwds]): 使用非阻塞方式调用func()函数( 并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func()函数的参数列表,kwds为传递给func()函数的关键字参数列表。
- apply(func[, args[, kwdsI): 使用阻塞方式调用func() 函数。
- close(): 关闭Pool, 使其不再接受新的任务。
- terminate():不管任务是否完成,立即终止。
- join(): 主进程阻塞,等待子进程的退出,必须在close或terminate之后使用
.
在上面的方法提到apply async() 使用非阻塞方式调用函数,而apply0使用阻塞方式调用函数。那么什么又是阻塞和非阻塞呢?在图中,分别使用阻塞方式和非阻塞方式执行3个任务。如果使用阻塞方式,必须等待上一个进程退出才能执行下一一个进程,而使用非阻塞方式,则可以并行执行3个进程。
在这里插入图片描述
下面通过一个示例演示一下 如何使用进程池创建多进程。这里模拟水池放水的场景,定义一个进程池,设置最大进程数为3。然后使用非阻塞方式执行10个任务,查看每个进程执行的任务。具体代码如下:
from multiprocessing import Pool # 导入
import os
import time
def task(name):
print(f"子进程{os.getpid()}执行task{name}...")
time.sleep(1)
if __name__ == "__main__":
# 1.创建进程池 最大进程数为3
pool = Pool(3)
# 2.从0开始执行10次
for i in range(10):
# 使用非阻塞方式调用task()函数
pool.apply_async(task, args=(i,))
print("等待所有子进程结束...")
pool.close() # 关闭进程池,关闭后pool不再接受新的任务请求
pool.join() # 等待子进程结束
print("所有子进程结束.")
运行结果如图所示,从图中可以看出PID为6420的子进程执行了4个任务,而其余2个子进程分别执行了3个任务。
4. 通过队列实现进程间通信
4.1 进程间不共享全局变量
我们已经学习了如何创建多进程,那么在多进程中,每个进程之间有什么关系呢?其实每个进程都有自己的地址空间、内存、数据栈以及其他记录其运行状态的辅助数据。下面通过一个例子,验证一下进程之间能否直接共享信息。
定义一个全局变量g_num,分别创建2个子进程对g_num执行不同的操作,并输出操作后的结果。代码如下:
import multiprocessing
# 定义一个全局变量
g_num = 100
# 加法任务
def plus():
print("----------子进程1开始-------------")
global g_num # 声明函数内部定义的变量为全局变量
g_num += 50
print(f"g_num is {g_num}")
print("----------子进程1结束-------------")
# 减法任务
def minus():
print("----------子进程2开始-------------")
global g_num # 声明函数内部定义的变量为全局变量
g_num -= 50
print(f"g_num is {g_num}")
print("----------子进程2结束-------------")
if __name__ == '__main__':
print("----------主进程开始-------------")
print(f"g_num is {g_num}")
# 1.创建进程对象
plus_process = multiprocessing.Process(target=plus)
minus_process = multiprocessing.Process(target=minus)
# 2.启动进程执行对应的任务
plus_process.start()
minus_process.start()
plus_process.join()
minus_process.join() # 等待子进程结束
print("----------主进程结束-------------")
运行结果如图所示:
上述代码中,分别创建了2个子进程,一个子进程中令g_num 加上50,另一个子进程令g_num减去50。但是从运行结果可以看出,g_num在父进程和2个子进程中的初始值都是100。也就是全局变量g_ num在一个进程中的结果,没有传递到下一个进程中,即进程之间没有共享信息。进程间示意图如图所示。
创建子进程会对主进程资源进行拷贝,也就是说子进程是主进程的一个副本,好比是一对双胞胎,之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已。
4.2 队列简介
通过上述了解到进程之间是不能进行通信的。那么要如何才能实现进程间的通信呢?Python的multiprocessing包装了底层的机制,提供了Queue (队列)、Pipes (管道)等多种方式来交换数据。这里将讲解通过队列(Queue)来实现进程间的通信。
队列(Queue) 就是模仿现实中的排队。例如学生在食堂排队买饭。新来的学生排到队伍最后,最前面的学生买完饭走开,后面的学生跟上。可以看出队列有两个特点:新来的学生都排在队尾。最前面的学生完成后离队,后面一个跟上。
4.3 多进程队列的使用
进程之间有时需要通信,操作系统提供了很多机制来实现进程间的通信。可以使用muliprocessing模块的Queue实现多进程之间的数据传递。Queue 本身是一个消息列队程序,下面介绍一下Queue的使用。
初始化Queue()对象时( 例如: q=Queue(num)),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头)。Queue 的常用方法如下:
- Queue.qsize(): 返回当前队列包含的消息数量。
- Queue.emptyO:如果队列为空,返回True;反之返回False。
- Queue.full(): 如果队列满了,返回True;反之返回False。
- Queue.get([block[, timeout]):获取队列中的一 条消息,然后将其从列队中移除,block 默认值为True。
如果block使用默认值,且没有设置timeout (单位秒),消息列队为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止。如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue. Empty"异常。
如果block值为False,消息列队为空,则会立刻抛出“Queue.Empty” 异常。
Queue.getnowait(): 相当Queue.get(False)。
Queue. put(item,[block[, timeout]):将item消息写入队列,block 默认值为True。
如果block使用默认值,且没有设置timeout (单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了timeout,则会等待timeout秒,若还没空间,则抛出“Queue.Full" 异常。
如果block值为False, 消息列队没有空间可写入,则会立刻抛出“Queue.Full” 异常。
Queue.put_nowait(item): 相当Queue.put(item, False)。
下面,通过一个例子学习一下如何使用processing.Queue。代码如下:
from multiprocessing import Queue
if __name__ == '__main__':
# 1.初始化一个Queue对象,最多可接受三条put消息
q = Queue(3)
q.put("消息1")
q.put("消息2")
print(q.full()) # 返回False
q.put("消息3")
print(q.full()) # 返回True
# 此时的话消息队列已满,下面的try都会抛出异常
# 第一个try会等待2秒后抛出异常,第二个try会立刻抛出异常
try:
q.put("消息4", True, 2)
except:
print(1)
# print("消息队列已满,现有消息数量:%s" % q.qsize())
pass
try:
q.put_nowait("消息4")
except:
# print(f"消息队列已满,现有消息数量: {q.qsize()}")
pass
# 读取消息时,先判断消息队列是否为空,再读取
if not q.empty():
print("--------从队列中读取消息-------------")
for i in range(q.qsize()):
print(q.get_nowait())
# 先判断消息队列是否已满,再写入
if not q.full():
q.put_nowait("消息4")
4.4 使用队列在进程间通信
我们知道使用multiprocessing.Process可以创建多进程,使用multiprocessing.Queue可以实现队列的操作。接下来,通过一个示例结合Proess和Queue实现进程间的通信。创建2个子进程,一个子进程负责向队列中写入数据,另一个子进程负责从队列中读取数据。为保证能够正确从队列中读取数据,设置读取数据的进程等待时间为2秒。如果2秒后仍然无法读取数据,则抛出异常。代码如下:
from multiprocessing import Process
from multiprocessing import Queue
import time
# 向队列中写入数据
def write_task(queue):
if not queue.full():
for i in range(5):
message = "消息" + str(i)
queue.put(message)
print(f"写入: {message}")
# 从队列中读取数据
def read_task(queue):
time.sleep(1) # 休眠1秒
while not queue.empty():
# 等待2秒,如果还没有读取到任何消息,则抛出"Queue.Empty"异常
print(f"读取: {queue.get(True, 2)}")
if __name__ == '__main__':
print("------------父进程开始---------------")
# 父进程创建Queue,并传给各个子进程
q = Queue()
# 实例化写入队列的子进程,并且传递队列
write_process = Process(target=write_task, args=(q,))
# 实例化读取队列的子进程,并且传递队列
read_process = Process(target=read_task, args=(q,))
# 开启进程执行对应的任务
write_process.start()
read_process.start()
# 等待子进程结束
write_process.join()
read_process.join()
print("------------父进程结束---------------")
运行结果如图:
5. 什么是线程
如果需要同时处理多个任务,一种是可以在一个应用程序内使用多个进程,每个进程负责完成一部分工作;另一种将工作细分为多个任务的方法是使用一个进程内的多个线程。那么,什么是线程呢?线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。例如,对于视频播放器,显示视频用一个线程,播放音频用另一个线程。只有2个线程同时工作,我们才能正常观看画面和声音同步的视频。
举一个生活中的例子来更好的理解进程和线程的关系。一个进程就像一座房子, 它是一个容器,有着相应的属性,如占地面积、卧室、厨房和卫生间等。房子本身并没有主动地做任何事情。而线程就是这座房子的居住者,他可以使用房子内每一个房间、做饭、洗澡等。
- 创建线程
由于线程是操作系统直接支持的执行单元,因此,高级语言(如Python、Java 等)通常都内置多线程的支持。Python的标准库提供了两个模块:_thread 和threading,_thread 是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
6.1 使用threading模块创建线程
threading模块提供了一个Thread类来代表一个线程对象,语法如下:
Thread([group [, target [ , name [, args [,kwargs]]] ]])。Thread类的参数说明如下:
group: 值为None,为以后版本而保留。
target: 表示一个可调用对象,线程启动时,run()方法将调用此对象,默认值为None, 表示不调用任何内容。
name : 表示当前线程名称,默认创建一个"Thread一N"格式的唯一名称。
args : 表示传递给target()函数的参数元组。
kwargs:表示传递给target()函数的参数字典。
对比发现,Thread 类和前面讲解的Porcess类的方法基本相同,这里就不再赘述了。下面,通过一个例子来学习一下如何使用threading 模块创建线程。代码如下:
import threading # 1.导入线程模块
import time
# 跳舞任务
def dance():
# 获取当前线程
print("dancethread:", threading.current_thread())
for i in range(4):
print("跳舞中....")
time.sleep(0.2)
# 唱歌任务
def sing():
# 获取当前线程
print("singthread:", threading.current_thread())
for i in range(4):
print("唱歌中....")
time.sleep(0.2)
if __name__ == '__main__':
# 获取当前线程
print("main_thread:", threading.current_thread())
# 1.创建子进程
dance_thread = threading.Thread(target=dance, name="dance_thread")
sing_thread = threading.Thread(target=sing, name="sing_thread")
# 2.启动子线程执行对应的任务
dance_thread.start()
sing_thread.start()
下面通过一个简单示例演示线程执行带有参数的任务:
import threading
import time
def show_info(name, age):
print(f"我的姓名是{name}, 年龄是{age}")
if __name__ == '__main__':
# 创建线程对象
# 以元组的形式传参 要保证元组里面元素的顺序和函数参数顺序保持一致
# sub_thread = threading.Thread(target=show_info, args=("Amo", 18))
# # 启动线程执行对应的任务
# sub_thread.start()
# 以字典的方式传参,保证字典里面的key要和函数参数名称一致,没有顺序要求
sub_thread = threading.Thread(target=show_info, kwargs={"age": 18, "name": "guiyin"})
# 启动线程执行对应的任务
sub_thread.start()
下面通过一个简单示例演示线程执行任务是无序的:
import threading
import time
def task():
time.sleep(1)
# 获取当前线程
print(threading.current_thread())
if __name__ == '__main__':
# 循环创建大量线程,测试线程之间执行是否无序
for i in range(20):
# 每循环一次创建一个子线程
sub_thread = threading.Thread(target=task)
# 启动子线程执行对应的任务
sub_thread.start()
# 注意: 线程间执行是无序的,具体哪个线程任务先执行是由cpu调度决定的
运行结果如下:
下面通过一个简单示例演示主线程会等待所有的子线程执行结束再结束:
import threading
import time
def task():
while True:
print("任务执行中...")
time.sleep(0.3)
if __name__ == '__main__':
# 创建子线程
# daemon=True 表示创建的子线程守护主线程,主线程退出子线程直接销毁
# subthread = threading.Thread(target=task, daemon=True)
sub_thread = threading.Thread(target=task)
# 把子线程设置成为守护主线程
sub_thread.setDaemon(True)
sub_thread.start()
# 主线程延时执行1秒
time.sleep(3)
print("over")
# exit()
# 结论: 主线程会等待子线程执行结束再结束
# 解决办法: 把子线程设置成为守护主线程即可
演示到这里,补充一个知识点,在进程实现多任务当中主进程同样会等待所有的子进程执行结束再结束,下面用一个简单的例子来说明:
import multiprocessing
import time
def task():
while True:
print("任务执行中....")
time.sleep(0.2)
if __name__ == '__main__':
# 1.创建子进程
# 把子进程设置为守护主进程,主进程退出子进程直接销毁
# subprocess = multiprocessing.Process(target=task, daemon=True)
subprocess = multiprocessing.Process(target=task)
# subprocess.daemon = True
# 2.启动子进程执行对应的任务
subprocess.start()
# 主进程延时2秒中
time.sleep(2)
# 退出主进程之前,先让子进程进行销毁
subprocess.terminate()
print("-----主进程结束-----")
# 结论: 主进程会等待子进程执行完成以后程序再退出
# 解决办法: 主进程退出子进程销毁
# 1.让子进程设置成为守护主进程,主进程退出子进程销毁,子进程会依赖主进程
# 2.让主进程退出之前先让子进程销毁
程序运行结果:
6.2 使用Thread子类创建线程
Thread线程类和Process进程类的使用方式非常相似,也可以通过定义一个子类,使其继承Thread线程类来创建线程。下面通过一个示例学习一下使用Thread子类创建线程的方式。创建一个子类SubThread,继承threading.Thread线程类,并定义一个run()方法。实例化SubThread类创建2个线程,并且调用start()方法开启线程,程序会自动调用run()方法。代码如下:
import threading
import time
class SubThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
msg = "子线程" + self.name + "执行,i=" + str(i) # name属性中保存的是当前线程的名字
print(msg)
if __name__== '__main__':
print("------主进程开始------")
t1 = SubThread()
t2 = SubThread()
t1.start()
t2.start()
t1.join()
t2.join()
print("------主进程结束------")
#执行顺序是计算机内部说的算
7. 线程间通信
7.1 线程间共享全局变量
在之前的学习中我们已经知道进程之间不能直接共享信息,那么线程之间可以共享信息吗?我们通过一个例子来验证一下。定义一个全局变量g mum, 分别创建2个子线程对g mum 执行不同的操作,并输出操作后.的结果。代码如下:
import threading
import time
# 定义一个全局变量
g_num = 100
# 加法任务
def plus():
print("----------子线程1开始-------------")
global g_num # 声明函数内部定义的变量为全局变量
g_num += 50
print(f"gnum is {g_num}")
print("----------子线程1结束-------------")
# 减法任务
def minus():
time.sleep(1)
print("----------子线程2开始-------------")
global g_num # 声明函数内部定义的变量为全局变量
g_num -= 50
print(f"g_num is {g_num}")
print("----------子线程2结束-------------")
if __name__ == '__main__':
print("----------主线程开始-------------")
print(f"g_num is {g_num}")
# 1.创建线程对象
plus_thread = threading.Thread(target=plus)
minus_thread = threading.Thread(target=minus)
# 2.启动线程执行对应的任务
plus_thread.start()
minus_thread.start()
plus_thread.join()
minus_thread.join() # 等待子线程结束
print("----------主线程结束-------------")
上述代码中,定义一个全局变量g_num,赋值为100,然后创建2个线程。一个线程将g_num增加50,一个线程将g_num减少50。如果g num的最终结果为100,则说明线程之间可以共享数据。运行结果如图所示:
从上面的例子可以得出,在一个进程内的所有线程共享全局变量,能够在不使用其他方式的前提下完成多线程之间的数据共享。但是这样的话线程之间共享全局变量数据就很容易出现错误,这里用一个简单的例子来说明:
import threading
import time
g_num = 0
# 循环100万次执行的任务
def task1():
for i in range(1000000):
global g_num # 表示此变量为全局变量
g_num += 1 # 每循环一次给全局变量加1
# 代码执行到此说明数据计算完成
print("task1:", g_num)
def task2():
for i in range(1000000):
global g_num # 表示此变量为全局变量
g_num += 1 # 每循环一次给全局变量加1
# 代码执行到此说明数据计算完成
print("task2:", g_num)
if __name__ == '__main__':
# 创建两个子线程
sub_thread1 = threading.Thread(target=task1)
sub_thread2 = threading.Thread(target=task2)
# 启动线程执行任务
sub_thread1.start()
# time.sleep(1)
sub_thread2.start()
程序运行结果:
在这里插入图片描述
按照正常结果来说,最后的结果应该是2000000,可是执行程序发现最后结果是小于2000000的,原因如下:
解决办法1:
== 线程等待,让第一个线程先执行,然后在让第二个线程再执行,保证数据不会有问题==
sub_thread1.join() # 主线程等待第一个子线程执行完成以后代码再继续往下执行
sub_thread2.start()
解决方法2:使用互斥锁
7.2 什么是互斥锁
由于线程可以对全局变量随意修改,这就可能造成多线程之间对全局变量的混乱操作。依然以房子为例,当房子内只有一个居住者时(单线程),他可以任意时刻使用任意一个房间,如厨房、卧室和卫生间等。但是,当这个房子有多个居住者时(多线程),他就不能在任意时刻使用某些房间,如卫生间,否则就会造成混乱。
如何解决这个问题呢? 一个防止他人进入的简单方法,就是门上加一把锁。先到的人锁上门,后到的人就在门口排队,等锁打开再进去。如图所示。
这就是“互斥锁“(Mutual exclusion,缩写Mutex),防止多个线程同时读写某一块内存区域。 互斥锁为资源引入一个状态:锁定和非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”时,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。但是这样的话其实多任务变为了单任务的操作,降低了程序执行效率。并且互斥锁使用不当的化,容易出现死锁现象。
7.3 使用互斥锁
在threading模块中使用Lock类可以方便地处理锁定。Lock 类有2个方法: acquire()锁定和release()释放锁。示例用法如下:
mutex = threading.Lock() # 创建锁
mutex.acquire([blocking]) # 锁定
mutex.release() # 释放锁
语法如下:
acquire([blocking]): 获取锁定,如果有必要,需要阻塞到锁定释放为止。如果提供blocking参数并将它设置为False,当无法获取锁定时将立即返回False;如果成功获取锁定则返回True。
release(): 释放一一个锁定。当锁定处于未锁定状态时,或者从与原本调用acquire()方法的不同线程调用此方法,将出现错误。
下面,通过一个示例学习一下如何使用互斥锁。这里使用多线程和互斥锁模拟实现多人同时订购电影票的功能,假设电影院某个场次只有100张电影票,10个用户同时抢购该电影票。每售出一 张,显示一次剩余的电影票张数。代码如下:
import threading
from threading import Lock
import time
n = 100 # 定义100张电影票
# 定义锁
mutex = Lock() # 创建互斥锁, Lock本质上是一个函数,通过调用函数可以创建一个互斥锁
def task():
global n
mutex.acquire() # 上锁
temp = n # 赋值给临时变量
time.sleep(0.1)
n = temp - 1 # 数量减1
print("购票成功,剩余%d张电影票" % n)
mutex.release() # 释放锁
if __name__ == '__main__':
t_l = [] # 初始化一个列表
for i in range(10):
t = threading.Thread(target=task)
t_l.append(t) # 将线程存入列表中
t.start() # 启动线程执行对应的任务
for t in t_l:
t.join() # 等待子线程结束
上述代码中,创建了10 个线程,全部执行task()函数。为解决资源竞争问题,使用mutex acquire()函数实现资源锁定,第一个获取资源的线程锁定后,其他线程等待mutex.release()解锁。所以每次只有一个线程执行task)函数。运行结果如图所示。
7.4 死锁
在这里插入图片描述
这里通过一个简单的例子来演示死锁,代码如下:
import threading
# 创建互斥锁
lock = threading.Lock()
# 需求: 多线程同时根据下标在列表中取值,要保证同一时刻只能有一个线程去取值
def get_value(index):
# 上锁
lock.acquire()
my_list = [1, 4, 6]
# 判断下标是否越界
if index >= len(my_list):
print("下标越界:", index)
# 取值不成功,也需要释放互斥锁,不要影响后面的线程去取值
# 锁需要在合适的地方进行释放,防止死锁
# lock.release()
return
# 根据下标取值
value = my_list[index]
print(value)
# 释放锁
lock.release()
if __name__ == '__main__':
# 创建大量线程,同时执行根据下标取值的任务
for i in range(10):
# 每循环一次创建一个子线程
sub_thread = threading.Thread(target=get_value, args=(i,))
# 启动线程执行任务
sub_thread.start()
运行结果如下:
7.5 使用队列在线程间通信
我们知道mulprocessing模块的Queue队列可以实现进程间通信,同样在线程间,也可以使用Queue:队列实现线程间通信。不同之处在于我们需要使用queue模块的Queue队列,而不是multprocessing模块的Queue队列,但Queue的使用方法相同。
使用Queue在线程间通信通常应用于生产者消费者模式。产生数据的模块称为生产者,而处理数据的模块称为消费者。在生产者与消费者之间的缓冲区称之为仓库。生产者负责往仓库运输商品,而消费者负责从仓库里取出商品,这就构成了生产者消费者模式。下面通过一个示例学习一下使用Queue在线程间通信。
定义一个生产者类Producer,定义一个消费者类Consumer。生成者生成5件产品,依次写入队列,而消费者依次从队列中取出产品,代码如下:
from queue import Queue
import random
import threading
import time
# 生产者类
class Producer(threading.Thread):
def __init__(self, name, queue):
threading.Thread.__init__(self, name=name)
self.data = queue
def run(self):
for i in range(5):
print("生产者%s将产品%d加入队列!" % (self.getName(), i))
self.data.put(i)
time.sleep(random.random())
print("生产者%s完成!" % self.getName())
# 消费者类
class Consumer(threading.Thread):
def __init__(self, name, queue):
threading.Thread.__init__(self, name=name)
self.data = queue
def run(self):
for i in range(5):
val = self.data.get()
print("消费者%s将产品%d从队列中取出!" % (self.getName(), val))
time.sleep(random.random())
print("消费者%s完成!" % self.getName())
if __name__ == '__main__':
print("一一一一一一主线程开始一一一一一一")
queue = Queue() # 实例化队列
producer = Producer("Producer", queue) # 实例化线程Producer,并传入队列作为参数
consumer = Consumer("Consumer", queue) # # 实例化线程Consumer,并传入队列作为参数
producer.start() # 启动线程Producer
consumer.start() # 启动线程Consumer
producer.join()
consumer.join()
print("一一一一一一主线程结束一一一一一")
程序运行结果如下:
8. 进程和线程对比
8.1 进程和线程对比的三个方向
- 关系对比
- 区别对比
- 优缺点对比
8.2 关系对比
线程是依附在进程里面的,没有进程就没有线程。
一个进程默认提供一条线程,进程可以创建多个线程。
在这里插入图片描述
8.3 区别对比
进程之间不共享全局变量
线程之间共享全局变量,但是要注意资源竞争的问题,解决办法: 互斥锁或者线程同步
创建进程的资源开销要比创建线程的资源开销要大
进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位
线程不能够独立执行,必须依存在进程中
多进程开发比单进程多线程开发稳定性要强
8.4 优缺点对比
进程优缺点:
优点:可以用多核
缺点:资源开销大
线程优缺点:
优点:资源开销小
缺点:不能使用多核
8.5 小结
进程和线程都是完成多任务的一种方式
多进程要比多线程消耗的资源多,但是多进程开发比单进程多线程开发稳定性要强,某个进程挂掉不会影响其它进程。
多进程可以使用cpu的多核运行,多线程可以共享全局变量。
线程不能单独执行必须依附在进程里面