• 并发编程之协程


    协程

    什么是协程

    ​ 在单个线程下实现并发效果,在多个任务之间切换。协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置,当程序中存在大量不需要CPU的操作时(IO),适用于协程。

    ​ 官方说法:协程称为微线程,就是操作系统级别的线程。是由操作系统来控制调度的。

    线程出现的问题

    1. GIL锁 导致多线程无法并行执行,只能并发执行,效率低。但是并发时我们要实现的最终目的(最好并行)

    2. 线程出现假死状态

    3. 例如tcp服务器,限制了最大线程数量1000,如果第1000个客户有一部分,没有进行任何的操作,而新任务将无法被处理,即使CPU空闲

    使用协程的好处

    1. 协程有极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销

    2. 不需要多线程的锁机制,因为只有一个线程,不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了。所以执行效率比多线程高很多。

    3. 因为协程是一个线程执行,所以想要利用多核CPU,最简单的方法是多进程+协程,这样既充分利用多核,又充分发挥协程的高效率。

    符合什么条件就能称之为协程:

    1. 必须在只有一个单线程里实现并发
    2. 修改共享数据不需加锁
    3. 用户程序里自己保存多个控制流的上下文栈
    4. 一个协程遇到IO操作自动切换到其它协程

    协程的使用场景

    1. IO密集型任务,且任务数量非常多。

    Python中对于协程有两个模块,greenlet和gevent。

    Greenlet(greenlet的执行顺序需要我们手动控制)

    第1阶段:无作为

    import  greenlet
    
    def task1():
        print("task1 start")
        time.sleep(2)
        print("task1 over")
    
    
    def task2():
        print("task2 start")
        time.sleep(2)
        print("task2 over")
    
    
    g1 = greenlet.greenlet(task1)
    g2 = greenlet.greenlet(task2)
    
    start_time = time.time()
    g1.switch()			# 可以理解为:开始g1协程,执行完毕后,开始g2协程
    g2.switch()			# 开始g2协程
    end_time = time.time() - start_time
    print(end_time)
    
    task1 start
    task1 over
    task2 start
    task2 over
    4.000658273696899
    

    不作为的情况下,就是串行。

    第2阶段:面对IO操作,手动切换协程(可以理解为并发)

    import  greenlet
    
    def task1():
        print("task1 start")
        g2.switch()		# 下面即将面对IO操作。于是切换到g2协程
        time.sleep(2)
        print("task1 over")
        g2.switch()		# g1协程执行完毕,切换到g2协程
    
    def task2():
        print("task2 start")
        g1.switch()		# 下面即将面对IO操作。于是切换到g1协程
        time.sleep(2)
        print("task2 over")
    
    g1 = greenlet.greenlet(task1)
    g2 = greenlet.greenlet(task2)
    
    # 计算时间
    start_time = time.time()
    g1.switch()			# 开启g1协程.这里注意g2协程已经在g1协程中开启了。所有在外面无需再开了
    end_time = time.time() - start_time
    
    print(end_time)
    
    task1 start
    task2 start
    task1 over
    task2 over
    4.000972509384155
    

    第3阶段:面对计算操作,手动切换协程(可以理解为并发,但是相对串行更复杂了)

    import  greenlet
    
    def task1():
        print("task1 start")
        g2.switch()
        for i in range(50000000):		
            1 + 1
        print("task1 over")
        g2.switch()
    
    def task2():
        print("task2 start")
        g1.switch()
        for i in range(50000000):
            1 + 1
        print("task2 over")
    
    g1 = greenlet.greenlet(task1)
    g2 = greenlet.greenlet(task2)
    
    # 计算时间
    start_time = time.time()
    g1.switch()
    end_time = time.time() - start_time
    
    print(end_time)
    
    task1 start
    task2 start
    task1 over
    task2 over
    2.6845099925994873
    

    第4阶段:面对计算操作,不切换协程(可以理解为串行)

    import  greenlet
    
    def task1():
        print("task1 start")
        for i in range(50000000):
            1 + 1
        print("task1 over")
    
    
    def task2():
        print("task2 start")
        for i in range(50000000):
            1 + 1
        print("task2 over")
    
    g1 = greenlet.greenlet(task1)
    g2 = greenlet.greenlet(task2)
    
    # 计算时间
    start_time = time.time()
    g1.switch()
    g2.switch()
    end_time = time.time() - start_time
    
    print(end_time)
    
    task1 start
    task2 start
    task1 over
    task2 over
    2.6445099925994873
    

    总结:

    1. 需要手动切换协程,增加了逻辑复杂度
    2. 效率不高,跟串行的效率相差不大

    gevent(自动切换,由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成)

    第1阶段:gevent对于io操作的处理方式(并发)

    from gevent.monkey import  patch_all	# 猴子补丁。geven实现并发的原理,是将原本阻塞的代码变为非阻塞
    import gevent
    import time
    patch_all()		# 对导入的模块进行打补丁
    
    def task1():
        print("task1 start")
        time.sleep(2)       # 遇到阻塞操作,切换到另外一个任务
        print("task1 over")
    
    def task2():
        print("task2 start")
        time.sleep(2)       # 遇到阻塞操作,切换到另外一个任务
        print("task2 over")
    
    g1 = gevent.spawn(task1)
    g2 = gevent.spawn(task2)
    
    # 计算时间
    start_time = time.time()
    g1.join()       # 开启一个  就能让所有的协程进行工作
    end_time = time.time() - start_time
    print(end_time)
    
    task1 start
    task2 start
    task1 over
    task2 over
    2.003842830657959
    

    第2阶段:gevent对于计算操作的处理方式(串行)

    from gevent.monkey import  patch_all
    import gevent
    import time
    patch_all()
    
    def task1():
        print("task1 start")
        for i in range(50000000):   # 此时遇到的不是IO操作,而是计算,因此协程不会切换,而是直接执行完
            1 + 1        
        print("task1 over")
    
    def task2():
        print("task2 start")
        for i in range(50000000):
            1 + 1               # 此时遇到的不是IO操作,而是计算,因此协程不会切换,而是直接执行完
        print("task2 over")
    
    
    g1 = gevent.spawn(task1)
    g2 = gevent.spawn(task2)
    
    # 计算时间
    start_time = time.time()
    g1.join()       # 开启一个  就能让所有的协程进行工作
    end_time = time.time() - start_time
    print(end_time)
    
    task1 start
    task1 over
    task2 start
    task2 over
    2.5767011642456055
    

    总结:

    1. 对于不同任务,采取的运行方式不同
      • 对于IO密集型操作,gevent模块采用的是并发的方式。即当协程中有IO操作(阻塞状态)时,任务立即切换到另外一个任务。
      • 对于计算密集型操作,gevent模块采用的是串行的方式。计算属于运行状态,因此协程不能切换。所以阶段二是串行的
    2. 与greenlet模块,优点:
      • 第一:gevent更智能。gevent模块遇到IO操作,自动切换协程。greenlet模块则需要手动切换
      • 第二:gevent更高效。对于不同阶段的时间可以看出,不论是IO密集型还是计算密集型,gevent消耗的时间更少
      • 第三:gevent简化了操作。不需要人为手动切换协程

    因此,协程使用gevent模块更好!

  • 相关阅读:
    Objective-C Memory Management Being Exceptional 异常处理与内存
    Objective-C Memory Management 内存管理 2
    c语言全局变量与局部变量(当变量重名时)的使用情况
    c语言指针字符串与字符数组字符串的区别
    c语言数组不同初始化方式的结果
    补码的用途
    struts2框架加载配置文件的顺序
    CSS盒子模型
    基于注解整合struts2与spring的时候如果不引入struts2-spring-plugin包自动装配无效
    @Resource注解省略name属性后的行为
  • 原文地址:https://www.cnblogs.com/plf-Jack/p/11153098.html
Copyright © 2020-2023  润新知