查看原文
其他

python协程系列(六)——asyncio+tkinter开发无阻塞程序以及timer的模拟

草yang年华 机器学习与python集中营 2021-09-10

python进阶教程

机器学习

深度学习

长按二维码关注

进入正文


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





声明:python协程系列文章的上一篇,即第六篇,详细介绍了asyncio的几个底层API概念,asyncio的事件循环EventLoop,Future类的详细使用,以及集中回答了关于异步编程的一些疑问,本文为系列文章的第七篇,将介绍如何使用多线程结合异步编程asyncio,开发出真正“不假死”的应用程序;以及如何模拟一个timer,实现定时操作。

另外,因为春节将至,小编也会休息一段时间,近期的公众号更新频率可能不会一天一更了,原创频率也会适当减少,假期过后会恢复的,还请各位小伙伴见谅。


目录

一 异步方法依然会假死(freezing)

1.1 一般程序的调用方“假死”

1.2 窗体程序的假死

二 多线程结合asyncio解决调用时的假死

2.1 asyncio专门实现Concurrency and Multithreading(多线程和并发)的函数介绍

2.2 不阻塞的多线程并发实例

2.3 tkinter+threading+asyncio

2.4 multithreading+asyncio总结

三 使用asyncio实现一个timer


01

异步方法依然会假死(freezing)

什么是程序的假死,这里不再多描述,特别是在编写桌面程序的时候,如果是使用单个线程,同步函数的方式,假死是不可避免的,但是有时候我们即使是使用了异步函数的方式依然是不可避免的,依然会假死,这是为什么呢,下面会通过几个例子来详细说明。


一般程序的调用方“假死”
import asyncio
import time
import threading

#定义一个异步操作
async def hello1(a,b):
    print(f"异步函数开始执行")
    await asyncio.sleep(3)
    print("异步函数执行结束")
    return a+b

#在一个异步操作里面调用另一个异步操作
async def main():
    c=await hello1(10,20)
    print(c)
    print("主函数执行")

loop = asyncio.get_event_loop()
tasks = [main()]
loop.run_until_complete(asyncio.wait(tasks))

loop.close()

'''运行结果为:
异步函数开始执行(在此处要等待3秒)
异步函数执行结束
30
主函数执行
'''

注意一个问题:我们前面所讲的例子中,没有出现等待,是因为各个异步方法之间是“完全并列”关系,彼此之间没有依赖,所以,我可以将所有的异步操作“gather”起来,然后通过事件循环,让事件循环在多个异步方法之间来回调用,永不停止,故而没有出现等待。
但是,现实中不可能所有的异步方法都是完全独立的,没有任何关系的,在上面的这个例子中,就是很好的说明,hello1是一个耗时任务,耗时大约3秒,main也是一个异步方法,但是main中需要用到hello1中的返回结果,所以他必须要等待hello1运行结束之后再才能继续执行,这就是为什么会得到上面结果的原因。这也再一次说明,异步依然是会有阻塞的。
我们也可以这样理解,因为我给事件循环只注册了一个异步方法,那就是main,当在main里面遇到了await,事件循环挂起,转而寻找其他的异步方法,但是由于只注册了一个异步方法给事件循环,他没有其他的方法可执行了,所以只能等待,让hello1执行完了,再继续执行。



窗体程序的假死

(1)同步假死

import tkinter as tk          # 导入 Tkinter 库
import time

class Form:
    def __init__(self):
        self.root=tk.Tk()
        self.root.geometry('500x300')
        self.root.title('窗体程序')  #设置窗口标题

        self.button=tk.Button(self.root,text="开始计算",command=self.calculate)
        self.label=tk.Label(master=self.root,text="等待计算结果")

        self.button.pack()
        self.label.pack()
        self.root.mainloop()

    def calculate(self):
        time.sleep(3)  #模拟耗时计算
        self.label["text"]=300

if __name__=='__main__':
    form=Form()

运行的结果就是,我单机一下“开始计算”按钮,然后窗体会假死,这时候无法移动窗体、也无法最大化最小化、3秒钟之后,“等待计算结果”的label会显示出3,然后前面移动的窗体等操作接着发生,最终效果如下:


上面的窗体会假死,这无可厚非,因为,所有的操作都是同步方法,只有一个线程,负责维护窗体状态的线程和执行好使计算的线程是同一个,当遇到time.sleep()的时候自然会遇到阻塞。那如果我们将耗时任务换成异步方法呢?代码如下:

(2)异步假死

import tkinter as tk          # 导入 Tkinter 库
import asyncio

class Form:
    def __init__(self):
        self.root=tk.Tk()
        self.root.geometry('500x300')
        self.root.title('窗体程序')  #设置窗口标题

        self.button=tk.Button(self.root,text="开始计算",command=self.get_loop)
        self.label=tk.Label(master=self.root,text="等待计算结果")

        self.button.pack()
        self.label.pack()

        self.root.mainloop()

    #定义一个异步方法,模拟耗时计算任务
    async def calculate(self):
        await asyncio.sleep(3)
        self.label["text"]=300

    #asyncio任务只能通过事件循环运行,不能直接运行异步函数
    def get_loop(self):
        self.loop=asyncio.get_event_loop()
        self.loop.run_until_complete(self.calculate())
        self.loop.close()


if __name__=='__main__':
    form=Form()

我们发现,窗体依然会造成阻塞,情况和前面的同步方法是一样的,为什么会这样呢?因为这个地方虽然启动了事件循环,但是拥有事件循环的那个线程同时还需要维护窗体的状态,始终只有一个线程在运行,当单击“开始计算”按钮,开始执行get_loop函数,在get_loop里面启动异步方法calculate,然后遇到await,这个时候事件循环暂停,但是由于事件循环只注册了calculate一个异步方法,也没其他事情干,所以只能等待,造成假死阻塞。

解决办法就是我专门再创建一个线程去执行一些计算任务,维护窗体状态的线程就之专门负责维护状态,后面再详说。



02

多线程结合asyncio解决调用时的假死


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 = future.result()   #等待获取Future的结果


不阻塞的多线程并发实例

asyncio.run_coroutine_threadsafe(coroutine,loop)的意思很简单,就是我在新线程中创建一个事件循环loop,然后在新线程的loop中不断不停的运行一个或者是多个coroutine。参考下面代码:

import asyncio 

import asyncio,time,threading

#需要执行的耗时异步任务
async def func(num):
    print(f'准备调用func,大约耗时{num}')
    await asyncio.sleep(num)
    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)

    new_loop = asyncio.new_event_loop()                        #在当前线程下创建时间循环,(未启用),在start_loop里面启动它
    t = threading.Thread(target=start_loop,args=(new_loop,))   #通过当前线程开启新的线程去启动事件循环
    t.start()

    asyncio.run_coroutine_threadsafe(coroutine1,new_loop)  #这几个是关键,代表在新线程中事件循环不断“游走”执行
    asyncio.run_coroutine_threadsafe(coroutine2,new_loop)
    asyncio.run_coroutine_threadsafe(coroutine3,new_loop)

    for i in "iloveu":
        print(str(i)+"    ")

if __name__ == "__main__":
    main()

'''运行结果为:
i    准备调用func,大约耗时3
l    准备调用func,大约耗时2
o    准备调用func,大约耗时1
v
e
u
耗时1之后,func函数运行结束
耗时2之后,func函数运行结束
耗时3之后,func函数运行结束
'''

我们发现,main是在主线程中的,而三个协程函数是在新线程中的,它们是在一起执行的,没有造成主线程main的阻塞。下面再看一下窗体函数中的实现。



tkinter+threading+asyncioimport tkinter as tk          # 导入 Tkinter 库
import time
import asyncio
import threading

class Form:
    def __init__(self):
        self.root=tk.Tk()
        self.root.geometry('500x300')
        self.root.title('窗体程序')  #设置窗口标题

        self.button=tk.Button(self.root,text="开始计算",command=self.change_form_state)
        self.label=tk.Label(master=self.root,text="等待计算结果")

        self.button.pack()
        self.label.pack()

        self.root.mainloop()

    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 = asyncio.new_event_loop()                        #在当前线程下创建时间循环,(未启用),在start_loop里面启动它
        t = threading.Thread(target=self.get_loop,args=(new_loop,))   #通过当前线程开启新的线程去启动事件循环
        t.start()

        asyncio.run_coroutine_threadsafe(coroutine1,new_loop)  #这几个是关键,代表在新线程中事件循环不断“游走”执行


if __name__=='__main__':
    form=Form()
运行上面的代码,我们发现,此时点击“开始计算”按钮执行耗时任务,没有造成窗体的任何阻塞,我可以最大最小化、移动等等,然后3秒之后标签会自动显示运算结果。为什么会这样?

上面的代码中,get_loop()、change_form_state()、__init__()都是定义在主线程中的,窗体的状态维护也是主线程,二耗时计算calculate()是一个异步协程函数。

现在单击“开始计算按钮”,这个事件发生之后,会触发主线程的chang_form_state函数,然后在该函数中,会创建新的线程,通过新的线程创建一个事件循环,然后将协程函数注册到新线程中的事件循环中去,达到的效果就是,主线程做主线程的,新线程做新线程的,不会造成任何阻塞。

tkinter+threading+asyncio第一步:定义需要异步执行的一系列操作,及一系列协程函数;
第二步:在主线程中定义一个新的线程,然后在新线程中产生一个新的事件循环;
第三步:在主线程中,通过asyncio.run_coroutine_threadsafe(coroutine,loop)这个方法,将一系列异步方法注册到新线程的loop里面去,这样就是新线程负责事件循环的执行。



03

使用asyncio实现一个timer

所谓的timer指的是,指定一个时间间隔,让某一个操作隔一个时间间隔执行一次,如此周而复始。很多编程语言都提供了专门的timer实现机制、包括C++、C#等。但是 Python 并没有原生支持 timer,不过可以用 asyncio.sleep 模拟。
大致的思想如下,将timer定义为一个异步协程,然后通过事件循环去调用这个异步协程,让事件循环不断在这个协程中反反复调用,只不过隔几秒调用一次即可。
简单的实现如下(本例基于python3.7):

async def delay(time):
    await asyncio.sleep(time)

async def timer(time,function):
    while True:
        future=asyncio.ensure_future(delay(time))
        await future
        future.add_done_callback(function)

def func(future):
    print('done')

if __name__=='__main__':
    asyncio.run(timer(2,func))

'''运行结果为:
done
done
done
done
done
done
done
done
done
done
done
.
.
.
.每隔2秒打印一个done
'''

几个注意点:asyncio.sleep()本身就是一个协程函数,故而可以将它封装成一个Task或者是Future,等待时间结束也就是任务完成,绑定回调函数。当然,本身python语法灵活,上面只是其中一种实现而已。

2019/01/28

Monday

2018年的寒假来临,在此,小编给大家说一声假期快乐,因为假期可能会走亲访友,时间没那么规律,可能公众号的更新频率会下降,原创文章可能会有所减少,还请各位海涵。假期结束之后,将会恢复到之前的状态。


推 荐 阅 读


python协程系列(六)——EventLoop和Future详解以及concurrency实现

python协程系列(五)——asyncio的核心概念与基本架构
python协程系列(四)——详解『同步|异步』『并发|并行』『线程|进程』
python协程系列(三)——yield from详解

python协程系列(二)——协程的通俗理解及yield关键字实现协程

Python协程系列(一)——从generator和yield表达式说起


赶紧关注我们吧

您的点赞和分享是我们进步的动力!

↘↘↘

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存