問:併發、並行、同步、異步、阻塞、非阻塞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異常。