zoukankan      html  css  js  c++  java
  • 23.并发编程之协程

    1.引子

    ​ 之前我们学习了线程、进程的概念。无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。

    ​ 而并发实现的原理是切换+保存,那就意味着使用多线程实现并发时,需要为每一个任务创建一个线程,必然增加了线程创建销毁与切换的带来的开销。高并发情况下,由于任务数量太多导致无法开启新的线程,使得即没有实际任务要执行,也无法创建新线程来处理新任务的情况。

    ​ 于是协程因此而出现,其原理是使用单线程来实现多任务并发。

    ​ 只要找到一种方案,能够在两个任务之间切换执行并且保存状态,那就可以实现单线程并发。

    于是我们可以利用生成器来实现并发执行:

    1. 通过yiled可以保存状态
    2. 把一个函数的结果传给另外一个函数
    3. 通过next可以取出
    4. 以此实现单线程内程序之间的切换
    def task1():
        while True:
            yield
            print("task1 run")
    
    def task2():
        g = task1()
        while True:
            next(g)
            print("task2 run")
    task2()
    

    2.greenlet模块实现并发

    ​ 使用yield来切换是的代码结构非常混乱,如果十个任务需要切换呢,不敢想象!因此就有人专门对yield进行了封装,这便有了greenlet模块。

    from greenlet import greenlet
    
    def eat(name):
        print('%s eat 1' %name)
        g2.switch('jack')
        print('%s eat 2' %name)
        g2.switch()
    def play(name):
        print('%s play 1' %name)
        g1.switch()
        print('%s play 2' %name)
    
    g1=greenlet(eat)
    g2=greenlet(play)
    
    g1.switch('rose')#可以在第一次switch时传入参数,以后都不需要再次传
    

    ​ 该模块简化了yield复杂的代码结构,实现了单线程下多任务并发,但是无论直接使用yield还是greenlet都不能检测IO操作,遇到IO时同样进入阻塞状态,而且对于纯计算任务而言效率也是没有任何提升的。

    3.协程

    ​ 协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

    1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
    2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)

    优点:

    1. 协程的切换开销更小,属于程序级别的切换,更加轻量级
    2. 单线程内就可以实现并发的效果,最大限度地利用cpu

    缺点:

    1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程来尽可能提高效率
    2. 协程本质是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

    4.gevent模块

    ​ 协程并发通过gevent模块实现,gevent中用到的主要模式是Greenlet,它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

    #用法
    #创建一个协程对象g1,
    g1=gevent.spawn(func,1,,2,3,x=4,y=5)
    #spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
    g2=gevent.spawn(func2)
    
    g1.join() #等待g1结束
    
    g2.join() #等待g2结束
    
    #或者上述两步合作一步:gevent.joinall([g1,g2])
    
    g1.value#拿到func1的返回值
    

    遇到IO阻塞时会自动切换任务

    import gevent,sys
    from gevent import monkey # 导入monkey补丁
    monkey.patch_all() # 打补丁 
    import time
    
    print(sys.path)
    
    def task1():
        print("task1 run")
        # gevent.sleep(3)
        time.sleep(3)
        print("task1 over")
    
    def task2():
        print("task2 run")
        # gevent.sleep(1)
        time.sleep(1)
        print("task2 over")
    
    g1 = gevent.spawn(task1)
    g2 = gevent.spawn(task2)
    #gevent.joinall([g1,g2])
    g1.join()
    g2.join()
    # 执行以上代码会发现不会输出任何消息
    # 这是因为协程任务都是以异步方式提交,所以主线程会继续往下执行,而一旦执行完最后一行主线程也就结束了,
    # 导致了协程任务没有来的及执行,所以这时候必须join来让主线程等待协程任务执行完毕   也就是让主线程保持存活
    # 后续在使用协程时也需要保证主线程一直存活,如果主线程不会结束也就意味着不需要调用join
    

    需要注意:

    1.如果主线程结束了 协程任务也会立即结束。

    2.monkey补丁的原理是把原始的阻塞方法替换为修改后的非阻塞方法,即偷梁换柱,来实现IO自动切换

    必须在打补丁后再使用相应的功能,避免忘记,建议写在最上方

    我们可以用threading.current_thread().getName()来查看每个g1和g2,查看的结果为DummyThread-n,即假线程

    monke补丁原理

    #myjson.py
    def dump():
        print("一个被替换的 dump函数")
    
    def load():
        print("一个被替换的 load函数")
    
    # test.py
    import myjson
    import json
    # 补丁函数
    def monkey_pacth_json():
        json.dump = myjson.dump
        json.load = myjson.load
        
    # 打补丁
    monkey_pacth_json()
    
    # 测试 
    json.dump()
    json.load()
    # 输出:
    # 一个被替换的 dump函数
    # 一个被替换的 load函数
    

    使用Gevent案例一 爬虫:

    from gevent import monkey;monkey.patch_all()
    import gevent
    import requests
    import time
    
    def get_page(url):
        print('GET: %s' %url)
        response=requests.get(url)
        if response.status_code == 200:
            print('%d bytes received from %s' %(len(response.text),url))
    
    
    start_time=time.time()
    gevent.joinall([
        gevent.spawn(get_page,'https://www.python.org/'),
        gevent.spawn(get_page,'https://www.yahoo.com/'),
        gevent.spawn(get_page,'https://github.com/'),
    ])
    stop_time=time.time()
    print('run time is %s' %(stop_time-start_time))
    

    使用Gevent案例二 TCP:

    #=====================================服务端
    from gevent import monkey;monkey.patch_all()
    from socket import *
    import gevent
    
    #如果不想用money.patch_all()打补丁,可以用gevent自带的socket
    # from gevent import socket
    # s=socket.socket()
    
    def server(server_ip,port):
        s=socket(AF_INET,SOCK_STREAM)
        s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
        s.bind((server_ip,port))
        s.listen(5)
        while True:
            conn,addr=s.accept()
            gevent.spawn(talk,conn,addr)
    
    def talk(conn,addr):
        try:
            while True:
                res=conn.recv(1024)
                print('client %s:%s msg: %s' %(addr[0],addr[1],res))
                conn.send(res.upper())
        except Exception as e:
            print(e)
        finally:
            conn.close()
    
    if __name__ == '__main__':
        server('127.0.0.1',8080)
    
    #=====================================多线程模拟多个客户端并发访问
    from threading import Thread
    from socket import *
    import threading
    
    def client(server_ip,port):
        c=socket(AF_INET,SOCK_STREAM) #套接字对象一定要加到函数内,即局部名称空间内,放在函数外则被所有线程共享,则大家公用一个套接字对象,那么客户端端口永远一样了
        c.connect((server_ip,port))
    
        count=0
        while True:
            c.send(('%s say hello %s' %(threading.current_thread().getName(),count)).encode('utf-8'))
            msg=c.recv(1024)
            print(msg.decode('utf-8'))
            count+=1
    if __name__ == '__main__':
        for i in range(500):
            t=Thread(target=client,args=('127.0.0.1',8080))
            t.start()
    
  • 相关阅读:
    Step by step Dynamics CRM 2013安装
    SQL Server 2012 Managed Service Account
    Step by step SQL Server 2012的安装
    Step by step 活动目录中添加一个子域
    Step by step 如何创建一个新森林
    向活动目录中添加一个子域
    活动目录的信任关系
    RAID 概述
    DNS 正向查找与反向查找
    Microsoft Dynamics CRM 2013 and 2011 Update Rollups and Service Packs
  • 原文地址:https://www.cnblogs.com/yellowcloud/p/11152243.html
Copyright © 2011-2022 走看看