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


      查看:https://blog.csdn.net/qq_27825451/article/details/86483493

      声明: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 = asyncio.get_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):
            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()
    

      解析:tk模板用来创建一个窗口,以下语句代码如果点击该按钮执行对应的函数,该函数模拟耗时计算,然后显示设置的值

    self.button=tk.Button(self.root,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):
            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
    
        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()
    
    # 异步假死 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 = future.result() #等待获取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 = asyncio.new_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)+"    ")
    
    if __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):
            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()
    # 函数窗体无阻塞 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 = asyncio.ensure_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语法灵活,上面只是其中一种实现而已。

  • 相关阅读:
    java多线程小节, 总结的不错
    奇瑞风云, 你还在路上么
    android NDK 环境建立
    外企下岗白领正成为“新4050”
    搭积木
    祝MORIENTES在LIVERPOOL有所成就
    简单生活
    为什么要更新
    归去来
    随记一笔
  • 原文地址:https://www.cnblogs.com/minseo/p/15469063.html
Copyright © 2020-2023  润新知