• 并发编程——协程


    协程:

      基于单线程来实现并发。

      协程并不是实际存在的实体,本质上是一个线程的多个部分。

      比线程的单位更小——协程,纤程,在一个线程中可以开启很多协程。

      在执行程序的过程中,遇到 IO 操作就冻结当前位置的状态,去执行其他任务,在执行其他任务的过程中,会不断地检测上一个冻结的任务是否 IO 结束,如果 IO 结束了,就继续从冻结的位置开始执行。

      一个线程不会遇到阻塞——一直在使用CPU。

      多个线程——只能有一个线程使用CPU。

      协程比线程之间的切换和线程的创建销毁所花费的时间,空间开销要小的多。

    协程的特点:冻结当前程序/任务的执行状态,可以规避IO操作的时间。

    import time
    def producer():
        res = []
        for i in range(1000000):
            res.append(i)
        return res
    
    def consumer(res):
        for i in res:pass
    
    start = time.time()
    res = producer()
    consumer(res)
    print(time.time()-start) # 0.26484227180480957
    
    def producer():
        for i in range(1000000):
            yield i
    def consumer():
        g = producer()
        for i in g:pass
    
    start = time.time()
    consumer()
    print(time.time() - start)  # 0.09993767738342285
    
    import time
    def consumer():
        while True:
            x=yield
    
    def producer():
        g=consumer()
        next(g)
        for i in range(10000000):
            g.send(i)
    start = time.time()
    producer()
    print(time.time() - start)  # 1.6259949207305908
    
    # 单纯的切换,还是要耗费一些时间的,记住当前执行的状态。
    # 用时间换了空间
    协程的引子

       协程:是单线程下的开发,又称微线程,纤程。

      协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

    需要强调的是:

      1,python的线程是属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行。)

      2,单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率。(!!!非io操作的切换与效率无关)

    对比操作系统控制线程的切换,用户在线程内控制协程的切换:

    优点如下:

      1,协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级。

      2,单线程内就可以实现并发的效果,最大限度地利用cpu.

    缺点如下:

      1,协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启多个协程。

      2,协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程。

    总结协程的特点:

      1,必须在只有一个单线程里实现并发。

      2,修改共享数据不需要加锁。

      3,用户程序里自己保存多个控制流的上下文栈。

      4,一个协程遇到 io 操作自动切换到其他协程

    Greenlet模块:

    安装:pip3 install greenlet

    import time
    from greenlet import greenlet
    
    def func1(name):
        print('%s'%name,123)
        g2.switch('小白')
        time.sleep(1)
        print('%s'%name,'abc')
    
    def func2(name):
        time.sleep(1)
        print('%s'%name,456)
        g1.switch()
    
    g1 = greenlet(func1)    # 实例化
    g2 = greenlet(func2)
    g1.switch('清秋') # 开始运行 可以在第一次switch时传入参数,以后就不用了。
    '''
    清秋 123
    小白 456
    清秋 abc
    '''
    greenlet实现状态切换

    单纯的切换(在没有io的情况下或者没有重复开辟内存空间的操作),反而会降低程序的执行速度。

    # 顺序执行
    import time
    
    def f1():
        res = 1
        for i in range(10000000):
            res += i
    
    def f2():
        res = 1
        for i in range(10000000):
            res *= i
    
    start = time.time()
    f1()
    f2()
    print(time.time()-start)    # 1.5120854377746582
    
    
    # 切换执行
    from greenlet import greenlet
    import time
    def f1():
        res = 1
        for i in range(10000000):
            res += i
            g2.switch()
    
    def f2():
        res = 1
        for i in range(10000000):
            res *= i
            g1.switch()
    
    start = time.time()
    g1 = greenlet(f1)
    g2 = greenlet(f2)
    g1.switch()
    print(time.time()-start)    # 1.9758000373840332
    
    # 由上可知,单纯的切换,反而会降低了程序的执行速度。
    效率对比

      greenlet 只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时,如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。

      单线程里的这20个任务的代码通常会既有计算操作,又有阻塞操作,所以我们可以在这些时间去执行其他任务,这样就能提高效率,这就用到了gevent模块。

    Gevent模块:

      安装:pip3 install gevent

      gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是greenlet,它是以C扩展模块形式介入Python的轻量级协程。greenlet全部运行在主程序操作系统进程的内部,但他们被协作式的调度。

    g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
    
    g2=gevent.spawn(func2)
    
    g1.join() #等待g1结束
    
    g2.join() #等待g2结束
    
    #或者上述两步合作一步:gevent.joinall([g1,g2])
    
    g1.value#拿到func1的返回值
    用法介绍
    from gevent import monkey;monkey.patch_all()
    # 它会把下面导入的所有模块中的IO操作都打成一个包,gevent就能够识别这些IO操作了。
    import time
    import gevent
    # 使用gevent模块来执行多个函数,表示在这些函数遇到IO操作的时候可以在同一个线程中进行切换。
    # 利用其他任务的IO阻塞时间来切换到其他的任务继续执行。
    # spawn来发布协程任务
    # gevent本身并不认识其他模块中的IO操作,所以只有 from gevent import monkey;monkey.patch_all() 才能识别
    # gevent就能够认是在这句话后导入模块的IO操作。
    
    from threading import currentThread
    def eat():
        print('eating1',currentThread())
        time.sleep(1)
        print('eating2')
    
    def play():
        print('playing1',currentThread())
        time.sleep(1)
        print('playing2')
    g1 = gevent.spawn(eat)
    g2 = gevent.spawn(play)
    time.sleep(1)  # 停一会等待执行完毕
    例子
    from gevent import monkey;monkey.patch_all()
    import time
    import gevent
    def eat(name):
        print('%s eat1' % name)
        time.sleep(1)
        print('%s eat2' % name)
    
    def play(name):
        print('%s play1' % name)
        time.sleep(1)
        print('%s play2' % name)
    
    g1 = gevent.spawn(eat,'egon')
    g2 = gevent.spawn(play,'alex')
    g1.join()
    g2.join()
    # 可以直接用 gevent.joinall([g1,g2])
    print('')
    遇到IO自动切换
    from gevent import monkey;monkey.patch_all()
    import threading
    import gevent
    import time
    def eat():
        print(threading.current_thread().getName()) # DummyThread-1
        print('eat1')
        time.sleep(2)
        print('eat2')
    def play():
        print(threading.current_thread().getName()) # DummyThread-2
        print('play1')
        time.sleep(2)
        print('play2')
    
    g1 = gevent.spawn(eat)
    g2 = gevent.spawn(play)
    gevent.joinall([g1,g2])
    print('')
    查看threading.current_thread().getName()

    gevent应用举例:

    from gevent import monkey;monkey.patch_all()
    import time
    import gevent
    from urllib.request import urlopen
    
    def get_page(url):
        res = urlopen(url)
        print(len(res.read()))
    
    url_lst = [
        'http://www.baidu.com',
        'http://www.sogou.com',
        'http://www.sohu.com',
        'http://www.qq.com',
        'http://www.cnblogs.com',
    ]
    start = time.time()
    gevent.joinall([gevent.spawn(get_page,url) for url in url_lst])
    print(time.time()-start) # 1.0084402561187744
    # 网页读取有一个机制,第一次读取的时候时间都会普遍久
    # 会将读取的网页缓存下来,以便下次的读取用。
    
    start = time.time()
    gevent.joinall([gevent.spawn(get_page,url) for url in url_lst])
    print(time.time()-start) # 0.5516667366027832
    
    start = time.time()
    for url in url_lst:
        get_page(url)
    print(time.time()-start)    # 1.533193588256836
    
    # 所以我们可以通过时间看出,协程爬取是比普通遍历快很多。
    协程应用,爬虫

    通过协程实现单线程下的socket并发:

    from gevent import monkey;monkey.patch_all()
    import socket
    import gevent
    
    def async_talk(conn):
        try:
            while True:
                conn.send(b'hello')
                ret = conn.recv(1024)
                print(ret)
                # 为了实现能够一直和同一个客户端聊天。
        finally:
            conn.close()  # 在程序报错的时候会关闭连接,节省空间
    
    sk = socket.socket()
    sk.bind(('127.0.0.1',9000))
    sk.listen()
    while True:
        conn,addr = sk.accept() # 因为循环,所以可重复接收多个多个客户端的连接
        gevent.spawn(async_talk,conn)   # 创建协程,将conn当参数传入函数
    
    sk.close()
    server
    import socket
    from threading import Thread
    
    def chat():
        sk = socket.socket()    # 放在函数内部,则每次一个线程就会有一个新的sk。
        sk.connect(('127.0.0.1',9000))
        while True:     # 循环对话。
            print(sk.recv(1024))
            sk.send(b'bye')
        sk.close()
    for i in range(500): # 创建500个线程客户端
        Thread(target=chat).start()
    client
  • 相关阅读:
    Django 初探--Django的开发服务器及创建数据库(笔记)
    Django初探--开发环境搭建(笔记)
    python文件处理--笔记
    python-property、docstring--笔记
    python之Lambda函数---笔记
    参数拆分-- 笔记
    python—带形状的词云图
    python—压缩文件
    python—dataframe用法
    python—mysql数据库读取表1获取name作为参数,传入访问表2获取age,结果存入excel
  • 原文地址:https://www.cnblogs.com/stfei/p/9052805.html
Copyright © 2020-2023  润新知