Python協程實踐

關於協程

 協程簡單來說就是一個更加輕量級的線程,並且不由操作系統內核管理,完全由程序所控制(在用戶態執行)。協程在子程序內部是可中斷的,然後轉而執行其他子程序,在適當的時候返回過來繼續執行。

協程的優勢?(協程擁有自己的寄存器上下文和棧,調度切換時,寄存器上下文和棧保存到其他地方,在切換回來的時候,恢復先前保存的寄存器上下文和棧,直接操作棧則基本沒有內核切換的開銷,可以不加鎖的訪問全局變量,所以上下文非常快。)

yield在協程中的用法

1、協程中的yield通常出現在表達式的右邊:

x = yield data

如果yield的右邊沒有表達式,默認產出的值是None,現在右邊有表達式,所以返回的是data這個值。
2、協程可以從調用法接受數據,調用通過send(x)方式將數據提供給協程,同時send方法中包含next方法,所以程序會繼續執行。
3、協程可以中斷執行,去執行另外的協程。

經典示例

代碼:

def hello():
    data = "mima"
    while True:
        x = yield data  
        print(x)
a = hello()
next(a)
data = a.send("hello")
print(data)

代碼詳解:
程序開始執行,函數hello不會真的執行,而是返回一個生成器給a。
當調用到next()方法時,hello函數纔開始真正執行,執行print方法,繼續進入while循環;
程序遇到yield關鍵字,程序再次中斷,此時執行到a.send("hello")時,程序會從yield關鍵字繼續向下執行,然後又再次進入while循環,再次遇到yield關鍵字,程序再次中斷;

額外

協程在運行過程中的四個狀態

  • GEN_CREATE:等待開始執行
  • GEN_RUNNING:解釋器正在執行
  • GEN_SUSPENDED:在yield表達式處暫停
  • GEN_CLOSED:執行結束

生產者-消費者模式(協程)

import time

def consumer():
    r = ""
    while True:
        res = yield r
        if not res:
            print("Starting.....")
            return
        print("[CONSUMER] Consuming %s...." %res)
        time.sleep(1)
        r = "200 OK"

def produce(c):
    next(c)
    n = 0
    while n<6:
        n+=1
        print("[PRODUCER] Producing %s ...."%n)
        r = c.send(n)
        print("[CONSUMER] Consumer return: %s ...."%r)
    c.close()

c = consumer()
produce(c)     

代碼分析:

  1. 調用next(c)啓動生成器;
  2. 消費者一旦生產東西,通過c.send切換到消費者consumer執行;
  3. consumer通過yield關鍵字獲取到消息,在通過yield把結果執行;
  4. 生產者拿到消費者處理過的結果,繼續生成下一條消息;
  5. 當跳出循環後,生產者不生產了,通過close關閉消費者,整個過程結束;

gevent第三方庫協程支持

原理:gevent基於協程的Python網絡庫,當一個greenlet遇到IO操作(訪問網絡)自動切換到其他的greenlet等到IO操作完成後,在適當的時候切換回來繼續執行。換而言之就是greenlet通過幫我們自動切換協程,保證有greenlet在運行,而不是一直等待IO操作。

經典代碼

由於切換時在發生IO操作時自動完成,所以gevent需要修改Python內置庫,這裏可以打上猴子補丁(用來在運行時動態修改已有的代碼,而不需要原有的代碼)monkey.patch_all

#!/usr/bin/python2
# coding=utf8

from gevent import monkey
monkey.patch_all()

import gevent
import requests

def handle_html(url):
    print("Starting %s。。。。" % url)
    response = requests.get(url)
    code = response.status_code

    print("%s: %s" % (url, str(code)))

if __name__ == "__main__":
    urls = ["https://www.baidu.com", "https://www.douban.com", "https://www.qq.com"]
    jobs = [ gevent.spawn(handle_html, url) for url in urls ]

    gevent.joinall(jobs)

運行結果:

Python協程實踐
結果:3個網絡連接併發執行,但是結束的順序不同。

asyncio內置庫協程支持

原理:asyncio的編程模型就是一個消息循環,從asyncio模塊中直接獲取一個Eventloop(事件循環)的應用,然後把需要執行的協程放入EventLoop中執行,實現異步IO。

經典代碼:

import asyncio
import threading

async def hello():
    print("hello, world: %s"%threading.currentThread())
    await asyncio.sleep(1) # 
    print('hello, man %s'%threading.currentThread())

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait([hello(), hello()]))
    loop.close()

代碼解析:

  • 首先獲取一個EventLoop
  • 然後將這個hello的協程放進EventLoop,運行EventLoop,它會運行知道future被完成
  • hello協程內部執行await asyncio.sleep(1)模擬耗時1秒的IO操作,在此期間,主線程並未等待,而是去執行EventLoop中的其他線程,實現併發執行。

代碼結果:
Python協程實踐

異步爬蟲實例:

#!/usr/bin/python3

import aiohttp
import asyncio

async def fetch(url, session):
    print("starting: %s" % url)
    async with session.get(url) as response:
        print("%s : %s" % (url,response.status))
        return await response.read()

async def run():
    urls = ["https://www.baidu.com", "https://www.douban.com", "http://www.mi.com"]
    tasks = []
    async with aiohttp.ClientSession() as session:
        tasks = [asyncio.ensure_future(fetch(url, session)) for url in urls] # 創建任務
        response = await asyncio.gather(*tasks) # 併發執行任務

        for body in response:
            print(len(response))

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(run())
    loop.close()

代碼解析:

  1. 創建一個事件循環,然後將任務放到時間循環中;
  2. run()方法中主要是創建任務,併發執行任務,返回讀取到的網頁內容;
  3. fetch()方法通過aiohttp發出指定的請求,以及返回 可等待對象;

Python協程實踐
(結束輸出網址和list中網址的順序不同,證明協程中異步I/O操作)

關於aiohttp

asyncio實現類TCP、UDP、SSL等協議,aiohttp則是基於asyncio實現的HTTP框架,由此可以用來編寫一個微型的HTTP服務器。

代碼:

from aiohttp import web

async def index(request):
    await asyncio.sleep(0.5)
    print(request.path)
    return web.Response(body='<h1> Hello, World</h1>')

async def hello(request):
    await asyncio.sleep(0.5)
    text = '<h1>hello, %s</h1>'%request.match_info['name']
    print(request.path)
    return web.Response(body=text.encode('utf-8'))

async def init(loop):
    app = web.Application(loop=loop)
    app.router.add_route("GET", "/" , index)
    app.router.add_route("GET","/hello/{name}", hello)
    srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
    print("Server started at http://127.0.0.0.1:8000....")
    return srv

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(init(loop))
    loop.run_forever()

代碼解析:

  1. 創建一個事件循環,傳入到init協程中;
  2. 創建Application實例,然後添加路由處理指定的請求;
  3. 通過loop創建TCP服務,最後啓動事件循環;

參考鏈接

https://www.liaoxuefeng.com/wiki/1016959663602400/1017985577429536
https://docs.aiohttp.org/en/stable/web_quickstart.html
https://docs.python.org/zh-cn/3.7/library/asyncio-task.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章