Python高級編程-協程和異步IO

第十一章:Python高級編程-協程和異步IO

Python3高級核心技術97講 筆記

目錄
第十一章:Python高級編程-協程和異步IO
11.1 併發、並行、同步、異步、阻塞、非阻塞
11.2 C10K問題和IO多路複用(select、poll、epoll)
11.2.1 C10K問題
11.2.2 Unix下五種I/O模型
11.3 select+回調+事件循環
11.4 回調之痛
11.5 什麼是協程
11.5.1 C10M問題
11.5.2 協程
11.6 生成器進階-send、close和throw方法
11.7生成器進階-yield from
11.8 yield from how
11.9 async和await
11.10 生成器實現協程
11.1 併發、並行、同步、異步、阻塞、非阻塞
併發

併發是指一個時間段內,有幾個程序在同一個CPU上運行,但是任意時刻只有一個程序在CPU上運行。

並行

並行是指任意時刻點上,有多個程序同時運行在多個CPU上。

同步

同步是指代碼調用IO操作是,必須等待IO操作完成才返回的調用方式。

異步

異步是指代碼調用IO操作是,不必等IO操作完成就返回的調用方式。

阻塞

阻塞是指調用函數時候當前線程被掛起。

非阻塞

阻塞是指調用函數時候當前線程不會被掛起,而是立即返回。

11.2 C10K問題和IO多路複用(select、poll、epoll)
11.2.1 C10K問題
如何在一顆1GHz CPU,2G內存,1gbps網絡環境下,讓單臺服務器同時爲一萬個客戶端提供FTP服務。

11.2.2 Unix下五種I/O模型
阻塞式IO

非阻塞IO

IO複用

信息驅動式IO

異步IO(POSIX的aio_系列函數

select、poll、epoll

select、poll、epoll都是IO多路複用的機制。IO多路複用就是通過一種機制,一個進程可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但是select、poll、epoll本質上都是同步IO,因爲他們都需要在讀寫時間就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步IO則無需自己負責進行讀寫,異步IO的實現會負責把數據從內核拷貝到用戶空間。

select

select函數監視的文件描述符分爲3類,分別是writefds、readfds、和exceptfds。調用後select函數會阻塞,直到有描述符就緒(有數據可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設爲null即可),函數返回。當select函數返回後,可以通過遍歷fdset,來找到就緒的描述符。

select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是他的一個優點。select的一個缺點在於單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般爲1024,可以通過修改宏定義甚至重新編譯內核的方式提升這一限制,但是這樣也會造成效率的降低。

poll

不同於select使用三個位圖來表示三個fdset的方式,pollshiyongyigepollfd的指針實現。

pollfd結構包含了要監視的event和發生的event,不再使用select“參數-值”傳遞的方式。同時,pollfd並沒有最大數量限制(但是數量過大後性能也是會下降)。和select函數一樣,poll返回後,需要倫輪詢pollfd來獲取就緒的描述符

從上面看,select和poll都需要在返回後,通過遍歷文件描述符來獲取已經就緒的socket。事實上,同時連接的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨着監視的描述符數量的增長,其效率也會線性下降。

epoll

epoll是在2.6內核中提出的,是之前的select和poll的增強版本。相對於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。

11.3 select+回調+事件循環
Copy

1. epoll並不代表一定比select好

在併發高的情況下,連接活躍度不是很高, epoll比select

併發性不高,同時連接很活躍, select比epoll好

通過非阻塞io實現http請求

import socket
from urllib.parse import urlparse

使用非阻塞io完成http請求

def get_url(url):

#通過socket請求html
url = urlparse(url)
host = url.netloc
path = url.path
if path == "":
    path = "/"

#建立socket連接
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.setblocking(False)
try:
    client.connect((host, 80)) #阻塞不會消耗cpu
except BlockingIOError as e:
    pass

#不停的詢問連接是否建立好, 需要while循環不停的去檢查狀態
#做計算任務或者再次發起其他的連接請求

while True:
    try:
        client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
        break
    except OSError as e:
        pass
data = b""
while True:
    try:
        d = client.recv(1024)
    except BlockingIOError as e:
        continue
    if d:
        data += d
    else:
        break

data = data.decode("utf8")
html_data = data.split("\r\n\r\n")[1]
print(html_data)
client.close()

if name == "__main__":

get_url("http://www.baidu.com")

Copy

1. epoll並不代表一定比select好

在併發高的情況下,連接活躍度不是很高, epoll比select

併發性不高,同時連接很活躍, select比epoll好

通過非阻塞io實現http請求

select + 回調 + 事件循環

併發性高

使用單線程

import socket
from urllib.parse import urlparse
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE

selector = DefaultSelector()

使用select完成http請求

urls = []
stop = False

class Fetcher:

def connected(self, key):
    selector.unregister(key.fd)
    self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8"))
    selector.register(self.client.fileno(), EVENT_READ, self.readable)

def readable(self, key):
    d = self.client.recv(1024)
    if d:
        self.data += d
    else:
        selector.unregister(key.fd)
        data = self.data.decode("utf8")
        html_data = data.split("\r\n\r\n")[1]
        print(html_data)
        self.client.close()
        urls.remove(self.spider_url)
        if not urls:
            global stop
            stop = True

def get_url(self, url):
    self.spider_url = url
    url = urlparse(url)
    self.host = url.netloc
    self.path = url.path
    self.data = b""
    if self.path == "":
        self.path = "/"

    # 建立socket連接
    self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.client.setblocking(False)

    try:
        self.client.connect((self.host, 80))  # 阻塞不會消耗cpu
    except BlockingIOError as e:
        pass

    #註冊
    selector.register(self.client.fileno(), EVENT_WRITE, self.connected)

def loop():

#事件循環,不停的請求socket的狀態並調用對應的回調函數
#1. select本身是不支持register模式
#2. socket狀態變化以後的回調是由程序員完成的
while not stop:
    ready = selector.select()
    for key, mask in ready:
        call_back = key.data
        call_back(key)
#回調+事件循環+select(poll\epoll)

if name == "__main__":

fetcher = Fetcher()
import time
start_time = time.time()
for url in range(20):
    url = "http://shop.projectsedu.com/goods/{}/".format(url)
    urls.append(url)
    fetcher = Fetcher()
    fetcher.get_url(url)
loop()
print(time.time()-start_time)

def get_url(url):

通過socket請求html

url = urlparse(url)

host = url.netloc

path = url.path

if path == "":

path = "/"

建立socket連接

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client.setblocking(False)

try:

client.connect((host, 80)) #阻塞不會消耗cpu

except BlockingIOError as e:

pass

不停的詢問連接是否建立好, 需要while循環不停的去檢查狀態

做計算任務或者再次發起其他的連接請求

while True:

try:

client.send("GET {} HTTP/1.1rnHost:{}rnConnection:closernrn".format(path, host).encode("utf8"))

break

except OSError as e:

pass

data = b""

while True:

try:

d = client.recv(1024)

except BlockingIOError as e:

continue

if d:

data += d

else:

break

data = data.decode("utf8")

html_data = data.split("rnrn")[1]

print(html_data)

client.close()

11.4 回調之痛
如果回調函數執行不正常該如何?

如果回調裏面還要嵌套回調該怎麼辦?要嵌套很多層怎麼辦?

如果嵌套了多層,其中某個環節出錯了會造成什麼後果?

如果有個數據需要被每個回調都處理怎麼辦?

....

可讀性差
共享狀態管理困難
異常處理困難
11.5 什麼是協程
11.5.1 C10M問題
如何利用8核心CPU,64G內存,在10gbps的網絡上保持1000萬併發連接

11.5.2 協程
Copy

def get_url(url):

do someting 1

html = get_html(url) #此處暫停,切換到另一個函數去執行

parse html

urls = parse_url(html)

def get_url(url):

do someting 1

html = get_html(url) #此處暫停,切換到另一個函數去執行

parse html

urls = parse_url(html)

傳統函數調用 過程 A->B->C

我們需要一個可以暫停的函數,並且可以在適當的時候恢復該函數的繼續執行

出現了協程 -> 有多個入口的函數, 可以暫停的函數, 可以暫停的函數(可以向暫停的地方傳入值)

11.6 生成器進階-send、close和throw方法
Copy
def gen_func():

#1. 可以產出值, 2. 可以接收值(調用方傳遞進來的值)
html = yield "http://projectsedu.com"
print(html)
return "bobby"

1. throw, close

1. 生成器不只可以產出值,還可以接收值

if name == "__main__":

gen = gen_func()
#在調用send發送非none值之前,我們必須啓動一次生成器, 方式有兩種1. gen.send(None), 2. next(gen)
url = gen.send(None)
#download url
html = "bobby"
print(gen.send(html)) #send方法可以傳遞值進入生成器內部,同時還可以重啓生成器執行到下一個yield位置
print(gen.send(html))
#1.啓動生成器方式有兩種, next(), send

# print(next(gen))
# print(next(gen))
# print(next(gen))
# print(next(gen))

Copy
def gen_func():

#1. 可以產出值, 2. 可以接收值(調用方傳遞進來的值)
try:
    yield "http://projectsedu.com"
except BaseException:
    pass
yield 2
yield 3
return "bobby"

if name == "__main__":

gen = gen_func()
print(next(gen))
gen.close()
print("bobby")

#GeneratorExit是繼承自BaseException, Exception

Copy
def gen_func():

#1. 可以產出值, 2. 可以接收值(調用方傳遞進來的值)
try:
    yield "http://projectsedu.com"
except Exception as e:
    pass
yield 2
yield 3
return "bobby"

if name == "__main__":

gen = gen_func()
print(next(gen))
gen.throw(Exception, "download error")
print(next(gen))
gen.throw(Exception, "download error")

11.7生成器進階-yield from
Copy

python3.3新加了yield from語法

from itertools import chain

my_list = [1,2,3]
my_dict = {

"bobby1":"http://projectsedu.com",
"bobby2":"http://www.imooc.com",

}

yield from iterable

def g1(iterable):

yield iterable

def g2(iterable):

yield from iterable

for value in g1(range(10)):

print(value)

for value in g2(range(10)):

print(value)

def my_chain(args, *kwargs):

for my_iterable in args:
    yield from my_iterable
    # for value in my_iterable:
    #     yield value

for value in my_chain(my_list, my_dict, range(5,10)):

print(value)

def g1(gen):

yield from gen

def main():

g = g1()
g.send(None)

1. main 調用方 g1(委託生成器) gen 子生成器

1. yield from會在調用方與子生成器之間建立一個雙向通道

Copy
final_result = {}

def middle(key):

while True:

final_result[key] = yield from sales_sum(key)

print(key+"銷量統計完成!!.")

def main():

data_sets = {

"bobby牌面膜": [1200, 1500, 3000],

"bobby牌手機": [28,55,98,108 ],

"bobby牌大衣": [280,560,778,70],

}

for key, data_set in data_sets.items():

print("start key:", key)

m = middle(key)

m.send(None) # 預激middle協程

for value in data_set:

m.send(value) # 給協程傳遞每一組的值 # 發送到字生成器裏

m.send(None)

print("final_result:", final_result)

if name == '__main__':

main()

def sales_sum(pro_name):

total = 0
nums = []
while True:
    x = yield
    print(pro_name+"銷量: ", x)
    if not x:
        break
    total += x
    nums.append(x)
return total, nums

if name == "__main__":

my_gen = sales_sum("bobby牌手機")
my_gen.send(None)
my_gen.send(1200)
my_gen.send(1500)
my_gen.send(3000)
try:
    my_gen.send(None)
except StopIteration as e:
    result = e.value
    print(result)

11.8 yield from how
Copy

pep380

1. RESULT = yield from EXPR可以簡化成下面這樣

一些說明

"""
_i:子生成器,同時也是一個迭代器
_y:子生成器生產的值
_r:yield from 表達式最終的值
_s:調用方通過send()發送的值
_e:異常對象

"""

_i = iter(EXPR) # EXPR是一個可迭代對象,_i其實是子生成器;
try:

_y = next(_i)   # 預激子生成器,把產出的第一個值存在_y中;

except StopIteration as _e:

_r = _e.value   # 如果拋出了`StopIteration`異常,那麼就將異常對象的`value`屬性保存到_r,這是最簡單的情況的返回值;

else:

while 1:    # 嘗試執行這個循環,委託生成器會阻塞;
    _s = yield _y   # 生產子生成器的值,等待調用方`send()`值,發送過來的值將保存在_s中;
    try:
        _y = _i.send(_s)    # 轉發_s,並且嘗試向下執行;
    except StopIteration as _e:
        _r = _e.value       # 如果子生成器拋出異常,那麼就獲取異常對象的`value`屬性存到_r,退出循環,恢復委託生成器的運行;
        break

RESULT = _r # _r就是整個yield from表達式返回的值。

"""

  1. 子生成器可能只是一個迭代器,並不是一個作爲協程的生成器,所以它不支持.throw()和.close()方法;
  2. 如果子生成器支持.throw()和.close()方法,但是在子生成器內部,這兩個方法都會拋出異常;
  3. 調用方讓子生成器自己拋出異常
  4. 當調用方使用next()或者.send(None)時,都要在子生成器上調用next()函數,當調用方使用.send()發送非 None 值時,才調用子生成器的.send()方法;
    """

_i = iter(EXPR)
try:

_y = next(_i)

except StopIteration as _e:

_r = _e.value

else:

while 1:
    try:
        _s = yield _y
    except GeneratorExit as _e:
        try:
            _m = _i.close
        except AttributeError:
            pass
        else:
            _m()
        raise _e
    except BaseException as _e:
        _x = sys.exc_info()
        try:
            _m = _i.throw
        except AttributeError:
            raise _e
        else:
            try:
                _y = _m(*_x)
            except StopIteration as _e:
                _r = _e.value
                break
    else:
        try:
            if _s is None:
                _y = next(_i)
            else:
                _y = _i.send(_s)
        except StopIteration as _e:
            _r = _e.value
            break

RESULT = _r

"""
看完代碼,我們總結一下關鍵點:

  1. 子生成器生產的值,都是直接傳給調用方的;調用方通過.send()發送的值都是直接傳遞給子生成器的;如果發送的是 None,會調用子生成器的__next__()方法,如果不是 None,會調用子生成器的.send()方法;
  2. 子生成器退出的時候,最後的return EXPR,會觸發一個StopIteration(EXPR)異常;
  3. yield from表達式的值,是子生成器終止時,傳遞給StopIteration異常的第一個參數;
  4. 如果調用的時候出現StopIteration異常,委託生成器會恢復運行,同時其他的異常會向上 "冒泡";
  5. 傳入委託生成器的異常裏,除了GeneratorExit之外,其他的所有異常全部傳遞給子生成器的.throw()方法;如果調用.throw()的時候出現了StopIteration異常,那麼就恢復委託生成器的運行,其他的異常全部向上 "冒泡";
  6. 如果在委託生成器上調用.close()或傳入GeneratorExit異常,會調用子生成器的.close()方法,沒有的話就不調用。如果在調用.close()的時候拋出了異常,那麼就向上 "冒泡",否則的話委託生成器會拋出GeneratorExit異常。

"""

11.9 async和await
Copy

python爲了將語義變得更加明確,就引入了async和await關鍵詞用於定義原生的協程

async def downloader(url):

return "bobby"

import types

@types.coroutine
def downloader(url):

yield "bobby"

async def download_url(url):

#dosomethings
html = await downloader(url)
return html

if name == "__main__":

coro = download_url("http://www.imooc.com")
# next(None)
coro.send(None)

11.10 生成器實現協程
Copy

生成器是可以暫停的函數

import inspect

def gen_func():

value=yield from

第一返回值給調用方, 第二調用方通過send方式返回值給gen

return "bobby"

1. 用同步的方式編寫異步的代碼, 在適當的時候暫停函數並在適當的時候啓動函數

import socket
def get_socket_data():

yield "bobby"

def downloader(url):

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.setblocking(False)

try:
    client.connect((host, 80))  # 阻塞不會消耗cpu
except BlockingIOError as e:
    pass

selector.register(self.client.fileno(), EVENT_WRITE, self.connected)
source = yield from get_socket_data()
data = source.decode("utf8")
html_data = data.split("\r\n\r\n")[1]
print(html_data)

def download_html(html):

html = yield from downloader()

if name == "__main__":

#協程的調度依然是 事件循環+協程模式 ,協程是單線程模式
pass

作者: coderchen01

出處:https://www.cnblogs.com/xunjishu/p/12864396.html

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