zoukankan      html  css  js  c++  java
  • 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语法灵活,上面只是其中一种实现而已。

  • 相关阅读:
    codevs 1115 开心的金明
    POJ 1125 Stockbroker Grapevine
    POJ 2421 constructing roads
    codevs 1390 回文平方数 USACO
    codevs 1131 统计单词数 2011年NOIP全国联赛普及组
    codevs 1313 质因数分解
    洛谷 绕钉子的长绳子
    洛谷 P1276 校门外的树(增强版)
    codevs 2627 村村通
    codevs 1191 数轴染色
  • 原文地址:https://www.cnblogs.com/minseo/p/15469063.html
Copyright © 2011-2022 走看看