Python說文解字_Python之多任務_04

問:併發、並行、同步、異步、阻塞、非阻塞html

答:程序員

  併發、並行:算法

  併發是指一個時間段內(不是指的時間點),有幾個程序在同一個CPU上運行,可是任意時刻只有一個程序在CPU上運行。對人類的時鐘來講1秒鐘能幹不少事,可是計算機1秒鐘運算上億次。讓我感受是不少程序一塊兒運行,實際上是一個程序在運行。編程

  並行是指任意時刻點(這裏這裏是時刻點 ),有不少個程序同時(這裏注意是同時 )在多個CPU上運行。緩存

  若是一個CPU是四核,咱們最高的並行是4核。服務器

  同步、異步:(涉及到IO操做的時候要考慮的,這是屬於消息操做的一種方式網絡

  同步是指代碼調用IO操做時,必須等待IO操做完成才能返回的調用方式。多線程

  異步是指代碼調用IO操做時,沒必要等待IO操做完成就返回的調用方法。(多線程就是典型的異步操做)併發

  阻塞、非阻塞:(涉及到IO操做的時候要考慮的,就是注意掛起的問題。這是屬於函數調用的一種方式app

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

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

 

問:一個問題的提出:C10K問題是什麼?

答:C10K問題是在1999年被提出來的技術挑戰:如何一顆1GHz CPU,2G內存,1gbps網絡環境下,讓單臺服務器同時爲1萬個客戶端提供FTP服務。

  在早期的互聯網用戶很是少,不會考慮到併發的問題。一個線程只能處理一個Socket,若是用這種模式不能讓一個服務器開啓上萬個客戶的。

 

問:UNIX下的五種IO模型:

答:

  阻塞式IO:

  非阻塞式IO:

  IO多路複用

  信號驅動式IO:如今使用很是少:

  異步IO(POSIX的aio_系列函數):

  這五種IO模式是一個遞進發展的關係。

 

問:阻塞式IO、非阻塞式IO:

答:好比以前的的socket編程就會遇到不少阻塞式IO

  1.client.connect(host,80)、client.recv(1024):

  就會遇到三次握手(關於三次握手四次揮手請參照:https://blog.csdn.net/li0978/article/details/52598121),這個過程其實是阻塞的,若是當前這個網絡鏈接不返回的話,會一直等待網絡數據的返回。IO的操做時間和CPU的時間差距很是大,對CPU的利用率很是低的,網絡中CPU的資源是很是重要的資源。時間浪費很是嚴重。

  好比咱們能夠設置client.setblocking(False)的話,connect會馬上返回。可是非阻塞式IO會帶來一些問題。若是網路鏈接沒有創建好,send會出問題的,這樣就不停的詢問鏈接是否創建好。connect阻塞不會消耗CPU的,可是咱們要進行後續的操做,咱們必需要肯定connect是否鏈接好了,因此這裏須要While True:循環一直詢問。可是While循環會消耗CPU的。其實還不如block讓它阻塞掉,可是以下下面的代碼不依賴於鏈接,這種非阻塞式IO就很是有用的。能夠轉而去作其餘的事情。

  內核:就是操做系統爲了保護內存,保留一部份內存給操做系統用,好比咱們在調用recvfrom函數是深刻到操做系統的函數,再去請求咱們的網絡,而後在拷貝到應用程序的緩存地址裏面。

 

問:繼續上面的問題:咱們將數據從內核複製到用戶空間,告訴咱們的程序準備好了呢?

答:這就是IO複用:

  select poll epoll是咱們最經常使用的三種命令方式。

  select的方法其實也是一種阻塞的方法。可是和咱們當時的While有不少的區別,他能夠監聽多個socket的狀態。前面只能監聽一個。監聽多個給咱們一個很是大的好處。是如今高併發技術應用的最多的點。可是從將數據從內核複製到用戶的空間仍是須要時間的。可是把不少步驟省略了。

 

問:信號驅動式IO

答:創建一個信號處理程序,是一種基於信號來的。可是如今應用很是少。

 

問:異步IO

答:aio開頭的。這是真正的異步IO。他會將數據從內核複製到用戶空間以後,再回發送信號處理程序處理數據報。是操做系統給咱們準備好了以後再發送。

 

問:IO複用中的select  poll  epoll:

答:IO複用和異步IO是如今比較經常使用的技術。這三個都是IO多路複用的機制。IO多路複用就是經過一種機制、一個進程能夠監視多個描述符,一旦某個描述符就緒(通常是就緒或者寫就緒),可以通知程序進行相應的讀寫操做。但select,poll,epoll本質上都是同步IO,由於它們都須要在讀寫時間就緒後本身負責進行讀寫,也就是說讀寫過程是阻塞的,而一部IO則無需本身負責進行讀寫,異步IO會負責把數據從內核拷貝到用戶空間。

  所以:IO多路複用(同步IO) vs  異步IO,它們之間是這麼一種關係。

 

問:select:

答:selcet函數監視的文件描述分三類:

  writefds

  readfds  

  exceptfds

  調用後,select函數會阻塞,知道有描述符就緒(有數據可讀、可寫或者有except)或者超時(timeout指定等待時間,若是馬上返回設爲null便可),函數返回。當selcet函數返回後,能夠遍歷fdset,來找到就緒的描述符。

  selcet目前幾乎在全部平臺上支持,其良好的跨平臺支持也是他的一個優勢。selcet的一個缺點在於單個繼承可以監視文件描述符的數量存在最大顯示,在Linux上通常爲1024,可是能夠經過修改宏定義甚至從新編譯內核的方式提高這一限制,可是這樣荷葉灰形成效率的極低。

 

問:poll:

答:不一樣於selct使用三個位圖來表示三個fdset的方式,poll使用一個pollfd的指針實現。

  pollfd解耦股包含了要監視的event和發生的event,再也不使用 select"參數-值"傳遞方式。同時pollfd並無最大數量限制(可是數量過大後性能也是會降低的)。和select函數同樣,poll返回後,須要輪詢pollfd來獲取就緒的描述符。

  從上面看,select和poll都須要在返回後,經過遍歷文件描述符來獲取已經就緒的socket。事實上,同時鏈接的大量客戶端在一時刻可能只有不多的處於就緒狀態,所以隨着監視的描述符的增加,其效率也會線性降低。

 

問:epoll:(在Linux下面支持,在Windows下面不支持的):

答:epoll是在2.6內核中提出的,是以前的select和poll的加強版本。相對於select和poll來講,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的實際存在到內核的一個事件表中,這樣用戶和內核控件的copy只需一次。epoll其實時使用了紅黑樹算法實現的。具體的紅黑樹算法見:https://www.cnblogs.com/skywang12345/p/3245399.html。epoll並不表明比select好。在併發高的狀況下,鏈接活躍度不高,epoll比select好。併發不高的話,同時鏈接很活躍,select比epoll好。

 

問:相關的舉例:

答:

  1.經過非阻塞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("utf-8")
    html_data = data.split("\r\n\r\n")[1]
    print(html_data)
    client.close()

if __name__ == '__main__':
    get_url("http://www.baidu.com")

 

  

  2. 經過IO複用的select的方法:

  咱們這裏使用selectors這個包的DefaultSelector的包,這個包比select包裝更好的,並且選擇poll方法和epoll方法會根據平臺自動選擇。還給咱們提供了註冊的機制。

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

selector = DefaultSelector()

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("utf-8")
            html_data = data.split("\r\n\r\n")[1]
            print(html_data)
            self.client.close()

    def get_url(self,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 True:
        ready = selector.select()
        for key,mask in ready:
            call_back = key.data
            call_back(key)
    # 回調+時間循環+select(poll/epoll)

if __name__ == '__main__':
    fetcher = Fetcher()
    fetcher.get_url("http://www.baidu.com")
    loop()

  運行這段代碼的時候會提示:OSError: [WinError 10022] 提供了一個無效的參數。在Linux下面不會報錯。

  所以咱們在Windows底下添加兩個全局變量進行更改,就不會拋異常了:

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

selector = DefaultSelector()
urls = ["http://www.baidu.com"]
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("utf-8")
            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()
    fetcher.get_url("http://www.baidu.com")
    loop()

  這種方式的好處就是併發性高。

 

問:回調之痛?

答:回調會產生不少問題:

  若是回調函數執行不正常該如何?

  若是回調裏面還要嵌套回調怎麼辦?要嵌套不少層怎麼辦?

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

  若是有一個數據須要被每一個回調都處理怎麼辦?

  怎麼使用當前函數中的局部變量?

 

  總結回調的問題有三個方面:

  1.可讀性差

  2.共享狀態管理困難

  3.異常處理困難

 

問:C10M問題?

答:C10M問題是隨着互聯網的飛速發展,若是利用八核CPU,64G內存,在10gbps的網絡上保持10000併發並鏈接?C10K也知足不了咱們了。所以這裏就用到了協程了。

 

問:咱們有什麼樣的處理思路?

答:回調模式有不少的缺點,協程就是爲了編寫難的問題。咱們列舉一下當前存在的問題:

  1.回調模式編碼複雜度高

  2.同步編程的併發性不高

  3.多線程編程須要線程間同步,用的鎖的機制。

 

  怎麼解決?

  1.採用同步的方式去編寫異步的代碼

  2.使用單線程去切換任務:

    1.線程是由操做系統切換的,單線程切換意味着咱們須要程序員本身去調度任務

    2.再也不須要鎖,併發性高。若是咱們能在單線程直接切換就好像函數之間的調用同樣,若是單線程內切換函數,性能遠高於線程切換,並且它的併發性更高。若是咱們聲明1000個函數比聲明1000個線程併發性越高。

  要實現這些對現有的編程模式有很大的挑戰。

 

問:一個關於函數的問題,關於函數是否能夠暫停:

答:好比咱們有這麼一段代碼:

def get_url(url):
    # do something:
    html = get_html(url)  # 此處暫停,切換到另外一個函數去執行
    # parse html
    urls = parse_url(html)

def get_url2():
    # do something:
    html = get_html(url)  # 此處暫停,切換到另外一個函數去執行
    # parse html
    urls = parse_url(html)

  咱們想一下:

  傳統函數調用過程就是A執行完執行B執行完執行C。

  咱們須要一個能夠暫停的函數,而且能夠在適當的時候回覆該函數繼續去執行。

  若是可以這兩個,是否是能夠玩兒了。所以這個地方就出現了協程

  協程有兩個定義:有多個入口的函數或者說能夠暫停的函數(能夠向暫停的地方傳入值),咱們感受到生成器就是一個能夠暫停的概念。

 

問:還記得咱們的生成器怎麼用嘛?

答:咱們用生成器對象,而後next去調用。生成器是使用了咱們迭代協議的。

def gen_func():
    yield 1
    yield 2
    yield 3
    return "bobby"

if __name__ == '__main__':
    gen = gen_func() # 咱們創建一個生成器對象
    print(next(gen))
    print(next(gen))
    print(next(gen))
    print(next(gen))
StopIteration: bobby

 

  1. 生成器不僅能夠產生值,還能夠接收值  

def gen_func():
# 1.能夠產出值,2.能夠接收值(調用方船體進來的值)
html = yield "http://www.baidu.com"
print(html)
yield 2
yield 3
return "bobby"

if __name__ == '__main__':
gen = gen_func() # 咱們創建一個生成器對象

# 1.啓動生成器方法又兩種,next和send
url = next(gen)
html = "bobby111"
print(gen.send(html))
# 2. send方法能夠傳遞值,進入生成器內部,同時還能夠重啓生成器執行到下一個yield位置。
gen.send(html)

# bobby111
# 2

  將咱們的值傳遞給生成器內部,這裏要注意的地方:第一次調用send的時候不能send一個非None的值,跟前面的同樣,首先要給生成器進行初始化對象。在調用send發送非None值以前,咱們必須啓動一次生成器,方式有兩種:

  gen.send(None)

  next(gen)

  其中,html = yield 內容,跟咱們說的同樣,yield看作是return,把yield後面的值傳遞給html

def gen_func():
    yield "http://www.baidu.com"
    yield 2
    yield 3
    return "bobby"

if __name__ == '__main__':
    gen = gen_func()
    print(next(gen))
    gen.close()
    next(gen)

    # 拋出異常:StopIteration:generator ignored GeneratotExit

 

  咱們看到他會拋異常。GeneratorExit是繼承自BaseException,Exception是比他們更基礎的,若是Try Except的話 。gen.close()不會拋異常。close()向上拋關閉。

def gen_func():
    try:
        yield "http://www.baidu.com"
    except Exception as e:
        print(e)
    yield 2
    yield 3
    return "bobby"

if __name__ == '__main__':
    gen = gen_func()
    print(next(gen))
    gen.throw(Exception,"download error")
    next(gen)
    # gen.throw(Exception,"download error")

    # http: // www.baidu.com
    # download
    # error

   另外,gen.throw的方式會拋第一個異常,須要處理,可是close不須要處理。

 

  咱們上面介紹了生成器的新的內容send,close,throw。

 

  咱們在介紹一個在Py3.3添加了yield from語法。

  首先介紹一下chain,這個是把一些可迭代對象,鏈接起來。用for循環進行遍歷。

from itertools import chain

my_list = [1,2,3]
my_dict = {
    "bobby1":"http://www.baidu.com",
    "bobby2":"http://www,sina.com"
}

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

    # 1
    # 2
    # 3
    # bobby1
    # bobby2
    # 5
    # 6
    # 7
    # 8
    # 9

   咱們再來改寫一下一個函數來實現這個chain

from itertools import chain

my_list = [1,2,3]
my_dict = {
    "bobby1":"http://www.baidu.com",
    "bobby2":"http://www,sina.com"
}

def my_chain(*args,**kwargs):
    for my_iterable in args:
        for value in my_iterable:
            yield value


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

    # 1
    # 2
    # 3
    # bobby1
    # bobby2
    # 5
    # 6
    # 7
    # 8
    # 9

   這裏就有yield from將代碼進一步縮減:

yield from EXPR(能夠簡化)
1. 子生成器可能只是一個迭代器,並非一個做爲協程的生成器,因此它不支持.throw 和 .close方法。
2.若是自生成其支持.throw和close方法,可是子生成器內部,這兩個方法都會拋出異常。
3.調用方讓子生成器本身拋出異常。
4.當調用方使用next,send(None)函數,當調用方使用.send()發送非None值是,才調用子生成器.send方法。

 

from itertools import chain

my_list = [1,2,3]
my_dict = {
    "bobby1":"http://www.baidu.com",
    "bobby2":"http://www,sina.com"
}

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)

    # 1
    # 2
    # 3
    # bobby1
    # bobby2
    # 5
    # 6
    # 7
    # 8
    # 9

  再舉一個例子:

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)


# 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9

  

  再舉一個最重要的例子:

def g1(gen):
    yield from gen

def main():
    g = g1()
    g.send(None)

   在這個例子中才是yield from最重要的應用,也是最核心的點,含義有兩個方面:

  1. main爲調用方,

  2. g1(委託生成器)

  3. gen(子生成器)

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

  根據這個咱們舉一個yield from 比較詳細的一個例子:

final_result = {}

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

def middle(key):
    while True:
        final_result[key] = yield from sales_sum(key)
        print(key+"銷量統計完成!!")

def main():
    data_sets = {
        "bobby牌面膜":[1200,1500,3000],
        "bobby牌手機":[20,55,98,100],
        "bobby牌大一":[280,560,778,70]
    }
    for key,data_set in data_sets.items():
        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()

# bobby牌面膜銷量: 1200
# bobby牌面膜銷量: 1500
# bobby牌面膜銷量: 3000
# bobby牌面膜銷量: None
# bobby牌面膜銷量統計完成!!
# bobby牌手機銷量: 20
# bobby牌手機銷量: 55
# bobby牌手機銷量: 98
# bobby牌手機銷量: 100
# bobby牌手機銷量: None
# bobby牌手機銷量統計完成!!
# bobby牌大一銷量: 280
# bobby牌大一銷量: 560
# bobby牌大一銷量: 778
# bobby牌大一銷量: 70
# bobby牌大一銷量: None
# bobby牌大一銷量統計完成!!
# final_result: {'bobby牌面膜': (5700, [1200, 1500, 3000]), 'bobby牌手機': (273, [20, 55, 98, 100]), 'bobby牌大一': (1688, [280, 560, 778, 70])}

  運用這個模式,就不用try exception處理異常了。就變得很是的簡單,不用作大量的try...exception了。yield from幫咱們完成了不少的工做。因此記住上面的那種格式。

  以下咱們對yield from作一個總結:

  1. 子生成器產生的值,都是直接傳給調用方的:調用方經過.send()發送的值都是直接傳遞給子生成器的:若是發送的是None,會調用子生成器__next__()方法,若是不是None,會調用子生成器的.send()方法。

  2.子生成器退出的時候,最後的return EXPR,會觸發一個StopIteration(EXPR)異常;

  3.yield from表達式的值,是子生成器終止時,傳遞給StopIteration異常的第一個參數。

  4.若是調用的時候出現了StopIteration異常,委託生成器會恢復運行,同時其餘的異常會向上「冒泡」;

  5.傳入委託生成器的異常裏,除了GenerationExit以外,其餘的全部異常所有傳遞給子生成器的throw()方法,若是調用.throw()的是出現了StopIteration異常,那麼就恢復委託生成器的運行,其餘的異常所有向上「冒泡」;

  6.若是在委託生成器上調用.close()或傳入GenerationExit異常,會調用自生成的.close()方法,沒有的話就不會調用。若是在調用.close()的時候拋出了異常,那麼就向上「冒泡」,不然委託生成器會拋出GenerationExit異常。

相關文章
相關標籤/搜索