18 线程
18.1 进程和线程
进程:打开一个程序至少就会有一个进程。操作系统进行资源分配的基本单位
线程:线程是CPU调度的基本单位,每个进程至少都有一个线程。
单线程:只有一个线程
1def funa(): 2 print(123) 3 4def funb(): 5 print(456) 6funa() 7funb() 8 9# 先执行funa 10# 再执行funb 11
多线程
线程模块: threading
import threading
线程类Thread参数:
target:执行的任务名
args: 以元组的形式给执行任务传参
1def funa(): 2 print(123) 3 time.sleep(2) 4 print("结束了") 5 6def funb(): 7 print(456) 8 time.sleep(3) 9 print("结束了") 10 11if __name__ == "__main__": 12 # 1创建子线程 13 t1 = threading.Thread(target=funa) #funa是个函数名 14 t2 = threading.Thread(target=funb) 15 16 # 2.开启子线程 17 t1.start() 18 t2.start()
带参数的执行
1def funa(a): 2 print("你好吗",a) 3 time.sleep(2) 4 print("我很好") 5 6def funb(b): 7 print(b) 8 time.sleep(2) 9 print("有点甜") 10 11 12if __name__ == "__main__": 13 # 第一种传入参数的方式 14 f1 = Thread(target=funa,args=("传入参数",)) 15 f2 = Thread(target=funa,args=("传入参数2",)) 16 17 f1.start() 18 f2.start() 19 20 21 # 第二种传入参数的方式 22 f1 = Thread(target=funa,kwargs={"a":"参数啊"}) 23 f2 = Thread(target=funa,kwargs={"b":"参数2"}) 24 25 f1.start() 26 f2.start() 27 28 29
18.2 线程
步骤
- 创建子线程 Thread()
- 开启子线程
18.2.1 守护线程 、阻塞线程
守护线程:主线程执行完,子线程立刻结束
阻塞线程:等待join_list中的子线程执行完,再执行主线程
1def funa(): 2 print("开始a") 3 time.sleep(2) 4 print("结束a") 5 6def funb(): 7 print("开始b") 8 time.sleep(2) 9 print("结束b") 10 11if __name__ == "__main__": 12 t1 = threading.Thread(target=funa) 13 t2 = threading.Thread(target=funb) 14 15 #开启守护线程,主线程执行完,子线程也会跟着结束 16 t1.setDaemon(True) 17 t2.setDaemon(True) 18 19 t1.start() 20 t2.start() 21 22 # 阻塞主线程,暂停的作用,只有join的执行完,才会执行主线程 23 t1.join() 24 t2.join() 25 26 t1.setName("线程1") 27 t2.setName("线程2") 28 29 # 获取线程名字 30 t1.getName() 31 t2.getName() 32 33 print("这是主线程,程序的最后一行") 34
18.2.2 线程的执行顺序的无序的
两个任务是一起执行的,线程之间的执行是无序的
1def test(): 2 time.sleep(1) 3 print("当前的线程是",threading,current_thread()) 4 5 6if __name__ =="__main__": 7 for i in range(5): 8 # 创建子线程 9 s1 = threading.Thread(target=test) 10 s1.start() 11 12
18.2.3 创建线程类
线程执行代码的封装
- 继承Thread类
- 重写run方法
1from threading import Thread 2 3# 定义一个线程类 4class Mythread(Thread): 5 6 # 重写run方法,规定run这个名字,表示线程活动的方法 7 def run(self): 8 print("面向对象") 9 time.sleep(3) 10 print("线程") 11 12if __name__ == "__main__": 13 my = MyThread() 14 my.start() 15
18.2.4 资源共享
1# 资源共享 2from threading import Thread #导入线程模块 3import time 4 5li = [] 6# 写入数据 7def wdata(): 8 for i in range(5): 9 li.append(i) 10 time.sleep(0.2) 11 print("写入的数据是:",li) 12 13# 读取数据 14def rdata(): 15 print("读取的数据是:",li) 16 17if __name__ == "__main__": 18 wd = Thread(target=wdata) 19 rd = Thread(target=radata) 20 wd.start() 21 22 wd.join() #只有等待写入完毕,才可以执行后面的代码 23 rd.start() 24 25 26 print("这是最后一行")
18.2.5 资源共享导致资源竞争
a是共享的资源,导致add和add2两个线程去竞争资源a,导致结果不一样。
1from threading import Thread 2 3a = 0 4n = 1000000 5# 循环b次给全局变量a加1 6def add(): 7 for i in range(n): 8 global a #global 声明全局变量 9 a += 1 10 print("第一次",a) 11 12def add2(): 13 for i in range(n): 14 global a #global 声明全局变量 15 a += 1 16 print("第二次",a) 17 18 19if __name__ == "__main__": 20 # 创建两个子线程 21 first = Thread(target=add) 22 second = Thread(target=add2) 23 24 # 启动线程 25 first.start() 26 second.start() 27 28# 运行结果 29# 第一次 1008170 30# 第二次 1509617 31
18.2.6 线程同步的方式
-
线程等待(join)
-
互斥锁
同步的概念:
有两个线程,线程A写入,线程B读取线程A写入的值;线程A先写入,线程B才能读取;线程A和B之间就是一种同步关系。
18.2.7 互斥锁
保证多个线程访问共享数据不会出现数据错误问题:保证同一时刻只能有一个线程去操作
threading模块里面定义了Lock这个函数,通过调用这个函数可以获取到一把互斥锁
互斥锁的作用
- 保证同一时刻只有一个线程去操作共享数据,不会出现错误问题。
- 使用互斥锁会影响代码的执行效率
如果互斥锁使用不当,就会出现死锁的状态。
acquire() 加锁
release() 解锁
加锁和解锁必须成对出现
1from threading import Thread,Lock 2 3a = 0 4n = 1000000 5# 循环b次给全局变量a加1 6 7# 创建互斥锁 8lock = Lock() 9 10def add(): 11 lock.acquire() #加锁 12 for i in range(n): 13 global a #global 声明全局变量 14 a += 1 15 print("第一次",a) 16 lock.release() #解锁 17 18def add2(): 19 lock.acquire() #加锁 20 for i in range(n): 21 global a #global 声明全局变量 22 a += 1 23 print("第二次",a) 24 lock.release() #解锁 25 26 27if __name__ == "__main__": 28 # 创建两个子线程 29 first = Thread(target=add) 30 second = Thread(target=add2) 31 32 # 启动线程 33 first.start() 34 second.start() 35 36
18.3 进程
18.2.1 进程介绍
运行一个程序就会有一个进程,一个进程默认有一个线程
进程:一个程序运行起来后,代码+用到的资源称之为进程,是操作系统分配资源的基本单位
进程的状态
- 就绪态:万事俱备,只欠cpu
- 执行态: cpu正在执行其功能
- 等待态:等待某些条件满足
1import time 2print("我们在学习进程") 3name = input("请输入你的名字") #用户输入,进行阻塞,等待态。 4print(name) #运行状态 5time.sleep(2) #睡眠2秒,阻塞状态 6print("对酒当歌,人生几何") #运行状态 7
18.3.2 进程创建
multiprocessing模块就是跨平台版本的多进程模块,提供了一个Process类来代表一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情。
1 2from multiprocessing import Process 3 4# Process 类 参数 5# target : 调用对象,子进程要执行的任务 6# args:以元组的形式传值 7# kwargs:以字典的形式传值 8 9 10# 常用的方法: 11# start() 开启子进程 12# is_alive() 判断子进程是否还活着,存活为True 13# join 主进程等待子进程执行完 14 15# 常用的属性 16# name 当前进程的别名 17# pid 当前进程的进程号 18 19
1import os 2from multiprocessing import Process 3 4def one(): 5 print("这是子进程一") 6 print(f"子进程id{os.getpid()},父进程id{os.getppid()}") 7 8def two(): 9 print("这是子进程二") 10 print(f"子进程id{os.getpid()},父进程id{os.getppid()}") 11 12if __name__ == "__main__": 13 # 创建子进程 14 p1 = Process(target=one,name="进程名称1") 15 p2 = Process(target=two) 16 17 #开启 18 p1.start() 19 p2.start() 20 21 print("p1的子进程名是:",p1.name) 22 print("p2的子进程名是: ",p2.name) 23 24 #查看子进程的进程号 25 print(p1.pid) 26 print(p2.pid) 27 28 print(f"主进程{os.getpid()},父进程:{os.getppid()}") 29 # 在cmd中,输入tasklist,找到pycharm.ext就可以看到进程号 30 31
is_alive() 和 join()
1 2def speak(name): 3 print(f"现在{name}在说话") 4 5def listen(name2): 6 print(f"{name2}在听课") 7 8if __name__ == "__main__": 9 10 p1 = Process(target=speak, args=('九歌',)) 11 p2 = Process(target=listen, args=('李四',)) 12 13 p1.start() 14 p1.join() #等待p1执行完,再执行后面的操作 15 p2.start() 16 17 18 print("p1的状态是:",p1.is_alive()) 19 print("p2的状态是:",p2.is_alive()) 20 21 22 23
18.3.3 进程的通信
进程间<font color="red">不共享</font>全局变量
1import time 2li = [] 3# 写入数据 4def wdata(): 5 for i in range(5): 6 li.append(i) 7 time.sleep(0.2) 8 print("写入的数据是",li) 9 10# 读取数据 11def rdata(): 12 print("读取的数据是:",li) 13 14if __main__ == "__main__": 15 p1 = Process(target=wdata) 16 p2 = Process(target=rdata) 17 18 p1.start() 19 p1.join() 20 p2.start() 21
进程的通信保证资源的传输
可以使用multiprocessing模块的Queue实现多进程之间的数据传输,Queue本身是一个消息队列程序
q.put() 放入数据
q.get() 取出数据
1 2# 入队 3q.put() #放入数据 4 5# 出队 6q.get() #取出数据 7 8# 导入模块 9from queue import Queue 10# 初始化一个队列对象 11q = Queue(3) #3表示最多可以接受3条消息 12q.put('我今天去输液了,输的什么液,想你的夜') 13q.put("你都不知道心疼人的") 14q.put("被一个人牵动着情绪很烦,但也可以很甜蜜") 15 16# get() #取出 17print(q.get()) 18print(q.get()) 19print(q.get()) 20 21 22# q.empty() 判断队列是否为空,为空返回True,否则为False 23# q.qsize() 队列中的数量 24# q.full() 判断队列是否满了,满了返回True 25 26print("目前的消息数量:",q.qsize()) 27
1 2# 通过队列来传递消息 3from multiprocessing import Process,Queue 4import time 5 6li = ["蒸羊羔","蒸熊掌","蒸花鸭"] 7 8# 写入数据 9def wdata(q): #q表示队列对象 10 for i in range(3): 11 print(f"将早餐{i}放进去") 12 q.put(i) 13 time.sleep(0.2) 14 15# 读取数据 16def rdata(q): #q表示队列对象 17 # 只要还有消息,就一直取出来 18 while True: 19 if q.empty(): #判断队列是否为空 20 break #跳出循环 21 else: 22 print("顾客从队列中获取到:",q.get()) 23 24 25 26if __name__ == "__main__": 27 # 创建队列对象 28 q =Queue() #省略里面的参数,没有大小限制 29 p1 = Process(target=wdata,args=(q,)) 30 p2 = Process(target=rdata,args=(q,)) 31 32 p1.start() 33 p2.start() 34 35
18.3.4 进程池
把子进程放到进程池之中
进程池的概念
定义一个池子,在里面放上固定数量的进程,有需求,就拿一个池子中的进程来处理任务
处理完毕,进程并不关闭,而是将这个进程再放回池子中继续等待任务
方法:
p.apply_async() 异步非阻塞,不用等待当前进程执行,随时根据系统调度来进行进程切换。
p.close() 关闭进程池
p.join() 主进程阻塞,等待所有工作进程退出,只能再close()后调用
1from multiprocessing import Pool 2import time 3 4def work(a): 5 print("我们在上课") 6 time.sleep(2) 7 return a * 3 8 9if __name__ == "__main__": 10 #定义一个进程池,最大进程数3 11 p = Pool(3) 12 13 li = [] 14 for i in range(6): 15 #p.apply_async(调用的目标,传递的参数) 16 res = p.apply_async(work,args=(i,)) #异步运行 17 # 异步:进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态 18 19 li.append(res) #将结果进行保存 20 print(res.get()) #打印结果 21 22 # 关闭进程池 23 p.close() 24 # 等待p进程池中所有子进程执行完,必须放在close方法后面 25 p.join() 26 27 # 使用get来获取apply_async的结果 28 for i in li: 29 print(i.get()) 30 31
18.4 协程
18.4.1 协程介绍
协程,又称为微线程,纤程。英文名Coroutine
协程是python中另外一个实现多任务的方式,只不过比线程占用更小执行单元(理解为需要的资源)。为啥说它是一个执行单元,因为它自带CPU上下文。这样只有在合适的时机,我们可以把一个协程切换到另一个协程。
只要这个过程中保存或恢复CPU上下文那么程序还是可以运行的。
协程:单线程下的并发,又称为微线程
对于协程来说,程序员就是上帝,你想让他执行到哪里,它就执行到哪里。
使用场景:
- 如果一个线程里面io操作比较多,协程就比较适用
- 适合用于高并发处理
简单实现协程
1import time 2 3def work1(): 4 while True: 5 yield '心疼哥哥' 6 7def work2(): 8 while True: 9 yield '互联网水太深,你把握不住' 10 11if __name__ == "__main__": 12 13 w1 = work1() 14 w2 = work2() 15 16 while True: 17 print(next(w1)) 18 print(next(w2)) 19# 程序员可以简单的通过代码,控制w1和w2的执行顺序 20
18.4.2 greenlet
greenlet: 是一个用C实现的协程模块,通过设置switch()来实现任意函数之间的切换
属于手动切换,当遇到IO操作,程序会阻塞,而不能进行自动切换
安装命令 pip install greenlet
卸载命令 pip uninstall ungreenlet
查看已安装模块的命令 pip list
1 2from greenlet import greenlet 3def eat(): 4 print("开始吃夜宵") 5 g2.switch() #切换到g2中运行,后面就不执行了 6 print("吃饱了") 7 8def study(): 9 print("开始学习") 10 print("学习完毕") 11 12 13# 实例化一个协程对象 14# greenlet(任务名) 15g1 = greenlet(eat) 16g2 = greenlet(study) 17 18g1.switch() # 切换到g1中运行 19 20 21# 输出结果 22# 开始吃夜宵 23# 开始学习 24# 学习完毕 25
18.4.3 gevent
greenlet是手动切换的,比较麻烦
gevent 则是自动切换的
gevent 遇到IO操作,会进行自动切换,属于主动式切换。
在gevent中用到的主要模式是greenlet
pip install gevent
1import gevent 2 3# 创建协程对象 4# gevent.spawn(函数名) 5# join 阻塞,等待某个协程执行完毕 6# joinall 参数是一个协程对象列表,会等待所有的协程都执行完毕再退出 7 8
执行A/B两个任务,当A、B遇到耗时操作,gevent会让A继续执行,同时也开始执行B任务
A完成了耗时操作后,B在对应的时间也完成了耗时操作
1# 切记,py文件不要跟第三方模块、内置模块重名 2 3import gevent 4def write(): 5 print("我们在写写成代码") 6 gevent.sleep(1) #等待的同时,开启其他所有协程,让所有协程(g1,g2)并发执行 7 print("终于写完了") 8 9def listen(): 10 print("现在先好好听课把") 11 gevent.sleep(1) 12 print("课间休息") 13 14g1 = gevent.spawn(write) 15g2 = gevent.spawn(listen) 16 17 18g1.join() #等待g1对象执行结束 19g2.join() #等待g2对象执行结束 20 21 22# 运行结果 23# 我们在写写成代码 24# 现在先好好听课把 25# 终于写完了 26# 课间休息 27
joinall 需要等待所有的协程对象执行完,再退出
1 2def work(name): 3 for i in range(3): 4 gevent.sleep(1) #等待的同时,开启其他所有协程,让所有协程并发执行 5 print(f'函数名是:{name},i的值是:{i}') 6 7gevent.joinall([ 8 gevent.spawn(work,'小白'), 9 gevent.spawn(work,'小鹅'), 10 11]) 12 13
18.4.4 打补丁
给程序打补丁
monkey补丁,
猴子补丁的功能:
- 拥有在模块中替换的功能
1from gevent import monkey 2import gevent 3import time 4 5 6monkey.patch_all() # 将time.sleep()代码,替换成gevent.sleep()代码 。 必须写在最前面 7 8def work(name): 9 for i in range(3): 10 # 将用到的耗时操作的代码,替换为gevent里面自己实现耗时操作的代码 11 time.sleep(1) 12 print(f'函数名是:{name},i的值是:{i}') 13 14gevent.joinall([ 15 gevent.spawn(work,'小白'), 16 gevent.spawn(work,'小鹅'), 17 18]) 19 20
18.4.5 综合例子
1import gevent 2 3def funa(): 4 print("wsc:今天有事情,跟孙yn打个电话") #1 5 gevent.sleep(2) 6 print("wsc: 怎么突然挂电话了,再打过去...") #5 7 8def funb(): 9 print("孙yn:wsc来电话啦。。。") #2 10 gevent.sleep(3) 11 print("孙yn: 他又打电话了") #6 12 13def func(): 14 print("亲爱的,你在干啥。。。") #3 15 gevent.sleep(1) 16 print("你过来呀") #4 17 18gevent.joinall([ 19 gevent.spawn(funa), 20 gevent.spawn(funb), 21 gevent.spawn(func) 22]) 23 24# 输出结果 25wsc:今天有事情,跟孙yn打个电话 26孙yn:wsc来电话啦。。。 27亲爱的,你在干啥。。。 28你过来呀 29wsc: 怎么突然挂电话了,再打过去... 30孙yn: 他又打电话了 31
总结
- 进程是资源分配的基本单位,线程是CPU调度的基本单位
- 对比:
- 进程:切换需要的资源最大,效率比较低
- 线程:切换需要的资源一般,效率一般
- 协程:切换需要的资源很小,效率较高
- 多线程:适合IO密集型操作(读写数据比较多,比如爬虫)
- 多进程:适合cpu密集型操作(科学计算,计算圆周率,对视频进行高清解码)
- 一个运行的程序至少有一个进程,一个进程至少有一个线程。
