Python-线程(3)-协程
Event事件
event 事件用来控制线程的执行,
由一些线程去控制另一些线程。
使用threading库中的Event对象,对象中包含了一个可由线程设置的信号标志,允许线程等待某些事件的发生
在初始情况下,Event对象中的信号标志位False,如果有线程等待一个Event对象,这个Event对象的标志为假,那么这个线程将会被一直阻塞直至标志为真。一个线程如果将一个Event对象信号标志设置为真,它将唤醒所有等待这个Event对象的线程。
# coding=utf-8
from threading import Event
from threading import Thread
import time
# 调用类实例化出对象
e = Event()
# 若程序中有如下代码,即为False,阻塞
# e.wait()
# 若程序中有如下代码,则将其他线程的False改为True,进入就绪态和运行态
# e.set()
# 模拟一个红绿灯
def light():
print("红灯亮")
time.sleep(5)
# 开始发信号给其他线程,告诉其他线程准备执行
e.set()
print("绿灯亮")
# 模拟一个个汽车
def car():
print("正在等红灯")
e.wait()
print("汽车开始起步")
t1 = Thread(target=light)
t1.start()
for i in range(10):
t2 = Thread(target=car)
t2.start()
红灯亮
正在等红灯
正在等红灯
正在等红灯
正在等红灯
正在等红灯
正在等红灯
正在等红灯
正在等红灯
正在等红灯
正在等红灯
绿灯亮
汽车开始起步
汽车开始起步
汽车开始起步
汽车开始起步
汽车开始起步
汽车开始起步
汽车开始起步
汽车开始起步
汽车开始起步
汽车开始起步
e.wait()
:False,为阻塞状态
e.set():True,将其他线程的False改为True,进入就绪态和运行态
e.clear():回复event的状态值为False
e.isSet():返回event的状态值
例如,有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作
from threading import Thread,Event
import threading
import time,random
def conn_mysql():
count=1
while not event.is_set():
if count > 3:
raise TimeoutError('链接超时')
print('<%s>第%s次尝试链接' % (threading.current_thread().getName(), count))
event.wait(0.5)
count+=1
print('<%s>链接成功' %threading.current_thread().getName())
def check_mysql():
print('\033[45m[%s]正在检查mysql\033[0m' % threading.current_thread().getName())
time.sleep(random.randint(2,4))
event.set()
if __name__ == '__main__':
event=Event()
conn1=Thread(target=conn_mysql)
conn2=Thread(target=conn_mysql)
check=Thread(target=check_mysql)
conn1.start()
conn2.start()
check.start()
线程池 进程池
在刚开始学多进程或多线程时,我们迫不及待地基于多进程或多线程实现并发的套接字通信,然而这种实现方式的致命缺陷是:服务的开启的进程数或线程数都会随着并发的客户端数目地增多而增多,这会对服务端主机带来巨大的压力,甚至于不堪重负而瘫痪,于是我们必须对服务端开启的进程数或线程数加以控制,让机器在一个自己可以承受的范围内运行,这就是进程池或线程池的用途,例如进程池,就是用来存放进程的池子,本质还是基于多进程,只不过是对开启进程的数目加上了限制
进程池和线程池:
用来控制当前程序允许创建(进程/线程)的数量
进程池和线程池的作用:
保证在硬件允许的范围内创建(进程/线程)的数量
# coding=utf-8
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
import time
# 进程池可以加参数 表示开启进程数
# 若不写默认以CPU的个数限制进程数
# ProcessPoolExecutor()
# 线程池可以加参数,表示开启的线程数
# 若不写默认以CPU 的个数 *5 限制线程数
# ThreadPoolExecutor()
# 创建5个线程
pool = ThreadPoolExecutor(5)
def task(res):
print("线程任务开始")
time.sleep(1)
# print("线程任务结束")
return 123
# 回调函数
def call_back(res):
# print(type(res))
# 注意 回调函数接收一个参数 是 接收线程执行完的结果,用.result()接收
# 得到的数据可以拿一个变量名保存,新的变量名不要与回调函数参数一样
res2 = res.result()
print(res2)
for i in range(13):
# 异步提交任务,每次并发执行最多只能有5个
pool.submit(task,1).add_done_callback(call_back)
# 所有线程任务结束后执行下面代码
pool.shutdown()
print("线程执行完毕了")
from concurrent.futures:提供了异步调用的接口
ProcessPoolExecutor():限制开启的进程数,若不写参数默认以CPU的个数限制进程数
ThreadPoolExecutor():限制开启的线程数,若不写参数默认以CPU的个数 * 5 限制线程数
pool.submit(函数名,参数):异步提交任务,限制每次并发执行最多的线程个数
add_done_callback:回调函数,线程执行完毕的函数返回值可以传到回调函数中,.result()获取线程执行返回的结果
pool.shutdown():所有线程任务执行完毕后执行线程池关闭,执行下面的代码,相当于进程池的pool.close()+pool.join()操作
进程池:
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import os,time,random
def task(n):
print('%s is runing' %os.getpid())
time.sleep(1)
return n**2
if __name__ == '__main__':
executor=ProcessPoolExecutor(max_workers=3)
futures=[]
for i in range(11):
future=executor.submit(task,i)
futures.append(future)
executor.shutdown(True)
print('+++>')
for future in futures:
print(future.result())
回调函数
可以为进程/线程池内的每个进程/线程绑定一个函数,该函数在进程或线程的任务执行完毕后自动触发,并接收线程任务的返回值当做参数,该函数就叫做回调函数
高性能爬取梨视频
from concurrent.futures import ThreadPoolExecutor
import requests
import re
import uuid
pool = ThreadPoolExecutor(200)
# 1.发送请求函数
def get_page(url):
response = requests.get(url)
return response
# 2.解析主页获取视频ID号
def parse_index(response):
id_list = re.findall('<a href="video_(.*?)".*?>',response.text,re.S)
return id_list
# 3.解析视频详情页获取真实 视频链接
def parse_detail(res):
response = res.result()
movie_detail_url = re.findall('srcUrl="(.*?)"', response.text, re.S)[0]
print(f'往视频链接: {movie_detail_url}发送请求...')
# 异步往视频详情页链接发送请求,把结果交给
pool.submit(get_page, movie_detail_url).add_done_callback(save_movie)
return movie_detail_url
# 4.往真实视频链接发送请求,获取数据并保存到本地
def save_movie(res):
movie_response = res.result()
name = str(uuid.uuid4())
print(f'{name}.mp4视频开始保存...')
with open(f'{name}.mp4', 'wb') as f:
f.write(movie_response.content)
print('视频下载完毕!')
if __name__ == '__main__':
# 1.访问主页获取数据
index_response = get_page('https://www.pearvideo.com/')
# # 2.解析主页获取所有的视频id号
id_list = parse_index(index_response)
# 3.循环对每个视频详情页链接进行拼接
for id in id_list:
detail_url = 'https://www.pearvideo.com/video_' + id
# 异步提交爬取视频详情页,把返回的数据,交给parse_detail(回调函数)
pool.submit(get_page, detail_url).add_done_callback(parse_detail)
协程
进程:资源单位
线程:执行单位
协程:在单线程下实现并发
协程即 基于单线程来实现并发,即只用一个主线程的情况下实现并发,是一种用户态的轻量级线程,是由用户程序自己控制调度的一张程序。
并发的概念:切换 + 保存状态
cpu正在运行一个任务,会有两种情况下切走去执行其他任务(切换操作由操作系统强制控制即多道技术),一种情况是该任务发生了阻塞,另外一种情况是该任务执行时间过长,cpu会把使用权切断。
yield保存状态
yield 代码级别的控制,可以保存当前状态
# 基于yield 并发执行
import time
# 任务1:接收数据,处理数据
def cousumer():
while True:
x = yield
def producer():
g = cousumer()
next(g)
for i in range(10000000):
g.send(i)
# time.sleep(1) # 并发去执行,但是如果遇到IO就会阻塞住
# 并不会切换到该线程内其他任务去执行
start = time.time()
# 基于yield保存状态,实现两个任务直接来回切换,即并发的效果
producer()
stop = time.time()
print(stop - start) # 2.9251673221588135
以上例子对纯计算密集型任务来说,对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。
总结协程特点:
1、 必须在只有一个单线程里实现并发
2、 修改共享数据不需要枷锁
3、 用户程序里自己保存多个控制流的上下文栈
gevent模块
from gevent import monkey
from gevent import spawn,joinall
import time
# gevent 是一个第三方模块,可以帮你监听IO操作,并切换
# 监听该程序下所有的IO操作
monkey.patch_all()
def func1():
print("1")
# 模拟IO操作
time.sleep(1)
def func2():
print("2")
time.sleep(2)
def func3():
print("3")
time.sleep(3)
start = time.time()
# 实现单线程下,遇到IO,保存状态 + 切换
s1 = spawn(func1)
s2 = spawn(func2)
s3 = spawn(func3)
# 发送信号,在单线程情况下相当于等待自己执行完毕之后再退出
joinall([s1,s2,s3])
end_time = time.time()
print(end_time - start) # 6.013344049453735
协程的目的
通过手动模拟操作系统“多道技术”,实现切换 + 保存状态
优点:在IO密集型的情况下会提高效率
缺点:在计算密集型的情况下,来回切换,反而效率更低
如何实现协程:切换 + 保存状态
yield:保存状态
并发:切换
TCP服务端单线程下实现并发
server端:
# coding=utf-8
from gevent import monkey
monkey.patch_all()
import socket
import time
from threading import Thread
from gevent import spawn,joinall
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen(5)
print("启动服务端...")
# 线程任务,接收客户端消息与发送消息给客户端
def work1(conn):
while True:
try:
data = conn.recv(1024).decode("utf-8")
if not data:break
conn.send(data.encode("utf-8"))
except Exception as e:
print(e)
break
conn.close()
def work2():
while True:
conn,addr = server.accept()
spawn(work1,conn)
if __name__ == '__main__':
s1 = spawn(work2)
s1.join()
client端:
# coding=utf-8
import socket
import time
from threading import Thread,current_thread
def client():
client = socket.socket()
client.connect(("127.0.0.1",8888))
print("启动客户端...")
num = 0
while True:
send_data = f"{current_thread().name} {num}"
client.send(send_data.encode("utf-8"))
data = client.recv(1024)
print(data.decode("utf-8"))
num += 1
for i in range(30):
t = Thread(target=client)
t.start()