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