python协程系列(七)——asyncio结合多线程解决阻塞问题以及timer模拟

足球怪史python协程系列(七)——asyncio结合多线程解决阻塞问题以
及timer模拟
  声明:python协程系列⽂章的上⼀篇,即第六篇,详细介绍了asyncio的⼏个底层API概念,asyncio的事件循环EventLoop,Future类的详细使⽤,以及集中回答了关于异步编程的⼀些疑问,本⽂为系列⽂章的第七篇,将介绍如何使⽤多线程结合异步编程asyncio,开发出真正“不假死”的应⽤程序;以及如何模拟⼀个timer,实现定时操作。
  ⼀,异步⽅法依然会假死(freezing)
  什么是程序的假死,这⾥不再多描述,特别是在编写桌⾯程序的时候,如果是使⽤当个线程,同步函数的⽅式,假死是不可避免的,但是有时候我们即使使⽤了异步函数的⽅式依然是不可避免的,依然会假死,这是为什么呢?下⾯会通过⼏个例⼦来详细说明。
  1,⼀般程序的调⽤⽅“假死”
  asyncio结合多线程解决阻塞问题以及timer模拟.py
# ⼀般程序的调⽤⽅假死 start
import asyncio
import time
import threading
async def hello1(a,b):
print('异步函数开始执⾏')
await asyncio.sleep(3)
print('异步函数执⾏结束')
return a + b
async def main():
c = await hello1(10,20)
print(c)
print('主函数执⾏')
loop = _event_loop()
tasks = [main()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
# ⼀般程序的调⽤⽅假死 end
  输出如下
异步函数开始执⾏
异步函数执⾏结束
30
主函数执⾏
  解析:
  执⾏异步主函数main,⾸先执⾏的是
c = await hello1(10,20)
  这个时候主函数阻塞,等待hello1(10,20)执⾏结束并返回结果,然后依次执⾏的是输出 ”异步函数开始执⾏” ,等待3秒再输出 “异步函数执⾏结束”,然后return a+b的值
  返回主函数main打印返回值为30 最后打印 “主函数执⾏”
  注意⼀个问题:我们前⾯所讲的例⼦中,没有出现等待,是因为各个异步⽅法之间是“完全并列”关系,彼此之间没有依赖,所以我们可以将所有异步操作“gathrer”起来,然后通过事件循环,让事件循环在多个异步⽅法之间来回调⽤,永不停⽌,故⽽没有出现等待。
  但是,现实中不可能所有的异步⽅法都是完全独⽴的,没有任何关系的,在上⾯的这个例⼦中,就是很好的说明,hello1是⼀个耗时任务,耗时⼤约为3秒,main也是⼀个异步⽅法,但是main中需要⽤到hello1中的返回结果,所以他必须要等到hello1运⾏结束之后才能继续执⾏,这就是为什么得到上⾯结果的原因。这也再⼀次说明,异步依然是会有阻塞的。
  我们也可以这样理解,因为我给事件循环只注册了⼀个异步⽅法,那就是main,当在main⾥⾯遇到await,事件循环挂起,转⽽寻其他的异步⽅法,但是由于只注册了⼀个异步⽅法给事件循环,没有其他⽅法可执⾏了,所以只能等待,让hello1执⾏完了,再继续执⾏。
  2,窗⼝程序的假死
  (1)同步假死
# 同步假死 start
import tkinter as tk          # 导⼊ Tkinter 库
import time
class Form:
def __init__(self):
<=tk.Tk()
self.button=tk.,text="开始计算",command=self.calculate)
self.label=tk.Label(,text="等待计算结果")
self.button.pack()
self.label.pack()
def calculate(self):
time.sleep(3)  #模拟耗时计算
self.label["text"]=300
if __name__=='__main__':
form=Form()
  解析:tk模板⽤来创建⼀个窗⼝,以下语句代码如果点击该按钮执⾏对应的函数,该函数模拟耗时计算,然后显⽰设置的值
self.button=tk.,text="开始计算",command=self.calculate)
  运⾏的结果会先显⽰⼀个窗⼝,然后单击"开始计算",这个时候就去执⾏对应的calculate()了,需要执⾏3秒,然后窗体会假死,这时候⽆法移动窗体,也⽆法最⼤化最⼩化,3秒之后,“等待计算结果”的label会显⽰300,然后前⾯在假死时进⾏的操作会接着发⽣,假如在假死的时候移动了窗⼝则窗⼝会移动⼀段距离,如果最⼩化则窗⼝会最⼩化,显⽰如下
  上⾯的窗体假死,这⽆可厚⾮,因为,所有的操作都是同步⽅法,只有⼀个线程,负责维护窗体状态的线程和执⾏计算的线程是同⼀个,当在执⾏计算遇到time.sleep()的时候⾃然会遇到阻塞。那如果我们将函数任务换成异步⽅法呢?代码如下:
  (2)异步假死
# 异步假死 start
import tkinter as tk          # 导⼊ Tkinter 库
import time
import asyncio
class Form:
def __init__(self):
<=tk.Tk()
self.button=tk.,text="开始计算",_loop)
self.label=tk.Label(,text="等待计算结果")
self.button.pack()
self.label.pack()
async def calculate(self):
await asyncio.sleep(3)  #模拟耗时计算
self.label["text"]=300
def get_loop(self):
self.loop = _event_loop()
self.loop.run_until_complete(self.calculate())
#self.loop.close()
if __name__=='__main__':
form=Form()
# 异步假死 end唐健生
  我们发现,窗体依然会造成阻塞,情况和前⾯的同步⽅法是⼀样的,为什么会这样呢?因为这个地
⽅虽然启动了事件循环,但是拥有事件循环的那个线程同时还需要维护窗体的状态,始终只有⼀个线程在运⾏,当单击“开始计算”按钮,开始执⾏get_loop函数,在get_loop⾥⾯启动异步⽅法calculate,然后遇到await,这个时候事件循环暂停,但是由于事件循环只注册了calculate⼀个异步⽅法,也没其他事情⼲,所以只能等待,造成假死阻塞。
  解决办法就是我专门再创建⼀个线程去执⾏⼀些计算任务,维护窗体状态的线程就只专门负责维护状态,后⾯再详说。
  ⼆,多线程结合asyncio解决调⽤时的假死
  1,asyncio咱们实现Concurrency and Multithreading(多线程和并发)的函数介绍
  为了让⼀个协程函数在不同的线程中执⾏,我们可以使⽤以下两个函数
  (1)loop.call_soon_threadsafe(callback, *args),这是⼀个很底层的API接⼝,⼀般很少使⽤,本⽂也暂时不做讨论。
  (2)asyncio.run_coroutine_threadsafe(coroutine,loop)
  第⼀个参数为需要异步执⾏的协程函数,第⼆个loop参数为在新线程中创建的事件循环loop,注意⼀
定要是在新线程中创建哦,该函数的返回值是⼀个 concurrent.futures.Future类的对象,⽤来获取协程的返回结果。
  future = asyncio.run_coroutine_threadsafe(coro_func(), loop) # 在新线程中运⾏协程
  result = sult() #等待获取Future的结果
  2、不阻塞的多线程并发实例
  asyncio.run_coroutine_threadsafe(coroutine,loop)的意思很简单,就是我在新线程中创建⼀个事件循环loop,然后在新线程的loop中不断不停的运⾏⼀个或者是多个coroutine。参考下⾯代码:
# 不阻塞的多线程实例 start
import asyncio
import asyncio,time,threading
# 需要执⾏的函数异步任务发挥余热
async def func(num):
print(f'准备调⽤func,⼤约耗时{num}')
await asyncio.sleep(num)
print(threading.currentThread())
print(f'耗时{num}之后,func函数运⾏结束')
# 定义⼀个专门创建事件循环的loop函数,在另⼀个线程中启动它
def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
# 定义⼀个main函数
def main():
coroutine1 = func(3)
coroutine2 = func(2)
coroutine3 = func(1)
# 在当前线程下创建事件循环(未启动),在start_loop⾥⾯启动
new_loop = w_event_loop()
# 通过当前线程开启新的线程去启动事件循环
t = threading.Thread(target=start_loop,args=(new_loop,))
t.start()
print(threading.currentThread())
# 这⼏条语句是关键,代表在新线程中事件循环不断"游⾛"执⾏
asyncio.run_coroutine_threadsafe(coroutine1,new_loop)
asyncio.run_coroutine_threadsafe(coroutine2,new_loop)
asyncio.run_coroutine_threadsafe(coroutine3,new_loop)
# 在main定义个循环⽤于检测main和协程是否同时执⾏
for i in 'iloveu':
print(str(i)+"    ")
索爱m600if __name__ == '__main__':
main()
# 不阻塞的多线程实例 end
  输出如下
<_MainThread(MainThread, started 11988)>
准备调⽤func,⼤约耗时3
i
准备调⽤func,⼤约耗时2
l
o
准备调⽤func,⼤约耗时1
v
e
u
<Thread(Thread-1, started 9876)>
耗时1之后,func函数运⾏结束
<Thread(Thread-1, started 9876)>
耗时2之后,func函数运⾏结束
<Thread(Thread-1, started 9876)>
耗时3之后,func函数运⾏结束
  可以看到主函数main的循环和3个协程⼏乎是同时执⾏,3个协程因为设置的执⾏时长不同所以执⾏结束时间有先后
  通过打印线程信息可以看到主线程id和协程的线程id是不⼀样的,即主函数main使⽤⼀个线程执⾏,3个协程使⽤新创建的线程执⾏
  我们发现,main是在主线程中的,⽽三个协程函数是在新线程中的,它们是在⼀起执⾏的,没有造成主线程main的阻塞。下⾯再看⼀下窗体函数中的实现。
# 函数窗体⽆阻塞 start
import tkinter as tk          # 导⼊ Tkinter 库
import time
import asyncio
import threading
class Form:
def __init__(self):
<=tk.Tk()
self.button=tk.,text="开始计算",command=self.change_form_state)
self.label=tk.Label(,text="等待计算结果")
self.button.pack()
self.label.pack()
async def calculate(self):
await asyncio.sleep(3)
self.label["text"]=300
def get_loop(self,loop):
self.loop=loop
asyncio.set_event_loop(self.loop)
self.loop.run_forever()
def change_form_state(self):
coroutine1 = self.calculate()
new_loop = w_event_loop()                        #在当前线程下创建事件循环,(未启⽤),在start_loop⾥⾯启动它
t = threading.Thread(_loop,args=(new_loop,))  #通过当前线程开启新的线程去启动事件循环
t.start()
asyncio.run_coroutine_threadsafe(coroutine1,new_loop)  #这⼏个是关键,代表在新线程中事件循环不断“游⾛”执⾏
if __name__=='__main__':
form=Form()
# 函数窗体⽆阻塞 end
  运⾏上⾯的代码,我们发现,此时点击“开始计算”按钮执⾏耗时任务,没有造成窗体的任何阻塞,我可以最⼤最⼩化、移动等等,然后3秒之后标签会⾃动显⽰运算结果。为什么会这样?
  上⾯的代码中,get_loop()、change_form_state()、__init__()都是定义在主线程中的,窗体的状态维护也是主线程,耗时计算calculate()是⼀个异步协程函数。
  现在单击“开始计算”按钮,这个事件发⽣之后,会触发主线程的chang_form_state函数,然后在该函数中,会创建新的线程,通过新的线程创建⼀个事件循环,然后将协程函数注册到新线程中的事件循环中去,达到的效果就是,主线程做主线程的,新线程做新线程的,不会造成任何阻塞。
  4、multithreading+asyncio总结
  第⼀步:定义需要异步执⾏的⼀系列操作,及⼀系列协程函数;
  第⼆步:在主线程中定义⼀个新的线程,然后再新线程中产⽣⼀个新的事件循环;氯化镍
  第三步:在主线程中,通过asyncio.run_coroutine_threadsade(coroutine,loop)这个⽅法,将⼀系列异步⽅法注册到新线程的loop⾥⾯去,这样就是新线程赋值事件循环的执⾏。
  三,使⽤asyncio实现⼀个timer
  所谓的timer指的是,指定⼀个时间间隔,让某⼀个操作隔⼀个时间间隔执⾏⼀次,如此周⽽复始。很多编程语⾔都提供了专门的timer 实现机制,包括C++,C#等。但是Python并没有原⽣⽀持timer,不过可以⽤asyncio.sleep模拟。
  ⼤致的思想如下,将timer定义为⼀个异步协程,然后同事件循环去调⽤这个异步协程,让事件循环不断在这个协程中反反复复调⽤,只不过隔⼏秒调⽤⼀次即可。
  简单的实现如下(本例基于python3.8):
# timer start
import asyncio
# 定义协程函数传递参数为整数然后休眠⼀段时间
async def delay(time):
await asyncio.sleep(time)
# 定义timer函数
# 该协程函数⽆限循环,⾸先创建⼀个future,该future调⽤协程函数delay
# 然后await future即等待delay(time)执⾏
# 然后调⽤回调函数function这个函数也是作为⼀个参数传递进来的
# future.all_done_all(fn) 附加可调⽤fn到future对象。当future对象取消或者运⾏完成时,调⽤fn
# ⽽这个future对象将作为它的唯⼀参数即函数fn只能有⼀个参数,这个参数就是future对象
async def timer(time,function):
while True:
future = sure_future(delay(time))
await future
future.add_done_callback(function)
# add_done_callback(fn)⽅法定义的函数只能有唯⼀参数,参数为future对象
内蒙讨吃调def func(future):
#print(future)
print('done')
if __name__ == "__main__":
asyncio.run(timer(2, func))
# timer end
  每隔2秒输出done,⽆限循环
done
done
done
done
done
done
done
done
done
done
.....
  调试模式分析
  ⼏个注意点:asyncio.sleep()本⾝就是⼀个协程函数,故⽽可以将它封装成⼀个Task或者Future,等待时间结束也就是任务完成,绑定回调函数。当然本⾝python语法灵活,上⾯只是其中⼀种实现⽽已。

本文发布于:2024-09-22 14:24:53,感谢您对本站的认可!

本文链接:https://www.17tex.com/xueshu/409752.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:函数   线程   事件   循环   阻塞
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议