最近在學python的網絡編程,學了socket通訊,並利用socket實現了一個具備用戶驗證功能,能夠上傳下載文件、能夠實現命令行功能,建立和刪除文件夾,能夠實現的斷點續傳等功能的FTP服務器。但在這當中,發現一些概念區分起來很難,好比並發和並行,同步和異步,阻塞和非阻塞,可是這些概念卻很重要。所以在此把它總結下來。python
併發:在操做系統中,是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。簡言之,是指系統具備處理多個任務的能力。shell
並行:當系統有一個以上CPU時,則線程的操做有可能非併發。當一個CPU執行一個線程時,另外一個CPU能夠執行另外一個線程,兩個線程互不搶佔CPU資源,能夠同時進行,這種方式咱們稱之爲並行(Parallel)。簡言之,是指系統具備同時處理多個任務的能力。數據庫
下面咱們來兩個例子:編程
import threading #線程 import time def music(): print('begin to listen music {}'.format(time.ctime())) time.sleep(3) print('stop to listen music {}'.format(time.ctime())) def game(): print('begin to play game {}'.format(time.ctime())) time.sleep(5) print('stop to play game {}'.format(time.ctime())) if __name__ == '__main__': music() game() print('ending.....')
music的時間爲3秒,game的時間爲5秒,若是按照咱們正常的執行,直接執行函數,那麼將按順序順序執行,整個過程8秒。服務器
import threading #線程 import time def music(): print('begin to listen music {}'.format(time.ctime())) time.sleep(3) print('stop to listen music {}'.format(time.ctime())) def game(): print('begin to play game {}'.format(time.ctime())) time.sleep(5) print('stop to play game {}'.format(time.ctime())) if __name__ == '__main__': t1 = threading.Thread(target=music) #建立一個線程對象t1 子線程 t2 = threading.Thread(target=game) #建立一個線程對象t2 子線程 t1.start() t2.start() # t1.join() #等待子線程執行完 t1不執行完,誰也不許往下走 t2.join() print('ending.......') #主線程 print(time.ctime())
在這個例子中,咱們開了兩個線程,將music和game兩個函數分別經過線程執行,運行結果顯示兩個線程同時開始,因爲聽音樂時間3秒,玩遊戲時間5秒,因此整個過程完成時間爲5秒。咱們發現,經過開啓多個線程,本來8秒的時間縮短爲5秒,本來順序執行如今是否是看起來好像是並行執行的?看起來好像是這樣,聽音樂的同時在玩遊戲,整個過程的時間隨最長的任務時間變化。但真的是這樣嗎?那麼下面我來提出一個GIL鎖的概念。網絡
GIL(全局解釋器鎖):不管你啓多少個線程,你有多少個cpu, Python在執行的時候會淡定的在同一時刻只容許一個線程運行。
import time from threading import Thread def add(): sum = 0 i = 1 while i<=1000000: sum += i i += 1 print('sum:',sum) def mul(): sum2 = 1 i = 1 while i<=100000: sum2 = sum2 * i i += 1 print('sum2:',sum2) start = time.time() add() mul() #串行比多線程還快 print('cost time %s'%(time.time()-start))
import time from threading import Thread def add(): sum = 0 i = 1 while i<=1000000: sum += i i += 1 print('sum:',sum) def mul(): sum2 = 1 i = 1 while i<=100000: sum2 = sum2 * i i += 1 print('sum2:',sum2) start = time.time() t1 = Thread(target=add) t2 = Thread(target=mul) l = [] l.append(t1) l.append(t2) for t in l: t.start() for t in l: t.join() print('cost time %s'%(time.time()-start))
哎吆,這是怎麼回事,串行執行比多線程還快?不符合常理呀。是否是顛覆了你的人生觀,這個就和GIL鎖有關,同一時刻,系統只容許一個線程執行,那麼,就是說,本質上咱們以前理解的多線程的並行是不存在的,那麼以前的例子爲何時間確實縮短了呢?這裏有涉及到一個任務的類型。多線程
--任務: 1.IO密集型(會有cpu空閒的時間) 注:sleep等同於IO操做, socket通訊也是IO
2.計算密集型
而以前那個例子剛好是IO密集型的例子,後面這個因爲涉及到了加法和乘法,屬於計算密集型操做,那麼,就產生了一個結論,多線程對於IO密集型任務有做用,
而計算密集型任務不推薦使用多線程。
而其中咱們還能夠獲得一個結論:因爲GIL鎖,多線程不可能真正實現並行,所謂的並行也只是宏觀上並行微觀上併發,本質上是因爲遇到io操做不斷的cpu切換
所形成並行的現象。因爲cpu切換速度極快,因此看起來就像是在同時執行。
--問題:沒有利用多核的優點
--這就形成了多線程不能同時執行,而且增長了切換的開銷,串行的效率可能更高。
對於一次IO訪問(以read舉例),數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。因此說,當一個read操做發生時,它會經歷兩個階段:
1. 等待數據準備 (Waiting for the data to be ready)
2. 將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)
同步:當進程執行IO(等待外部數據)的時候,-----等。同步(例如打電話的時候必須等)
異步:當進程執行IO(等待外部數據)的時候,-----不等,去執行其餘任務,一直等到數據接收成功,再回來處理。異步(例如發短信)
當咱們去爬取一個網頁的時候,要爬取多個網站,有些人可能會發起多個請求,而後經過函數順序調用。執行順序也是先調用先執行。效率很是低。
下面咱們看一下異步的一個例子:
import socket import select """ ########http請求本質,IO阻塞######## sk = socket.socket() #1.鏈接 sk.connect(('www.baidu.com',80,)) #阻塞 print('鏈接成功了') #2.鏈接成功後發送消息 sk.send(b"GET / HTTP/1.0\r\nHost: baidu.com\r\n\r\n") #3.等待服務端響應 data = sk.recv(8096)#阻塞 print(data) #\r\n\r\n區分響應頭和影響體 #關閉鏈接 sk.close() """ """ ########http請求本質,IO非阻塞######## sk = socket.socket() sk.setblocking(False) #1.鏈接 try: sk.connect(('www.baidu.com',80,)) #非阻塞,但會報錯 print('鏈接成功了') except BlockingIOError as e: print(e) #2.鏈接成功後發送消息 sk.send(b"GET / HTTP/1.0\r\nHost: baidu.com\r\n\r\n") #3.等待服務端響應 data = sk.recv(8096)#阻塞 print(data) #\r\n\r\n區分響應頭和影響體 #關閉鏈接 sk.close() """ class HttpRequest: def __init__(self,sk,host,callback): self.socket = sk self.host = host self.callback = callback def fileno(self): return self.socket.fileno() class HttpResponse: def __init__(self,recv_data): self.recv_data = recv_data self.header_dict = {} self.body = None self.initialize() def initialize(self): headers, body = self.recv_data.split(b'\r\n\r\n', 1) self.body = body header_list = headers.split(b'\r\n') for h in header_list: h_str = str(h,encoding='utf-8') v = h_str.split(':',1) if len(v) == 2: self.header_dict[v[0]] = v[1] class AsyncRequest: def __init__(self): self.conn = [] self.connection = [] # 用於檢測是否已經鏈接成功 def add_request(self,host,callback): try: sk = socket.socket() sk.setblocking(0) sk.connect((host,80)) except BlockingIOError as e: pass request = HttpRequest(sk,host,callback) self.conn.append(request) self.connection.append(request) def run(self): while True: rlist,wlist,elist = select.select(self.conn,self.connection,self.conn,0.05) for w in wlist: print(w.host,'鏈接成功...') # 只要能循環到,表示socket和服務器端已經鏈接成功 tpl = "GET / HTTP/1.0\r\nHost:%s\r\n\r\n" %(w.host,) w.socket.send(bytes(tpl,encoding='utf-8')) self.connection.remove(w) for r in rlist: # r,是HttpRequest recv_data = bytes() while True: try: chunck = r.socket.recv(8096) recv_data += chunck except Exception as e: break response = HttpResponse(recv_data) r.callback(response) r.socket.close() self.conn.remove(r) if len(self.conn) == 0: break def f1(response): print('保存到文件',response.header_dict) def f2(response): print('保存到數據庫', response.header_dict) url_list = [ {'host':'www.youku.com','callback': f1}, {'host':'v.qq.com','callback': f2}, {'host':'www.cnblogs.com','callback': f2}, ] req = AsyncRequest() for item in url_list: req.add_request(item['host'],item['callback']) req.run()
咱們能夠看到,三個請求發送順序與返回順序,並不同,這樣就體現了異步請求。即我同時將請求發送出去,哪一個先回來我先處理哪一個。併發
即咱們能夠理解爲:我打電話的時候只容許和一我的通訊,和這我的通訊結束以後才容許和另外一我的開始。這就是同步。app
咱們發短信的時候發完能夠不去等待,去處理其餘事情,當他回覆以後咱們再去處理,這樣就大大解放了咱們的時間。這就是異步。異步
體如今網頁請求上面就是我請求一個網頁時候等待他回覆,不然不接收其它請求,這就是同步。另外一種就是我發送請求以後不去等待他是否回覆,而去處理其它請求,當處理完其餘請求以後,某個請求說,個人回覆了,而後程序轉而去處理他的回覆數據。這就是異步請求。因此,異步能夠充分cpu的效率。
調用blocking IO會一直block住對應的進程直到操做完成,而non-blocking IO在kernel還準備數據的狀況下會馬上返回。
下面咱們經過socket實現一個命令行功能來感覺一下。
#服務端 from socket import * import subprocess import struct ip_port = ('127.0.0.1', 8000) buffer_size = 1024 backlog = 5 tcp_server = socket(AF_INET, SOCK_STREAM) tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 tcp_server.bind(ip_port) tcp_server.listen(backlog) while True: conn, addr = tcp_server.accept() print('新的客戶端連接:', addr) while True: try: cmd = conn.recv(buffer_size) print('收到客戶端命令:', cmd.decode('utf-8')) #執行命令cmd,獲得命令的結果cmd_res res = subprocess.Popen(cmd.decode('utf-8'),shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE, ) err = res.stderr.read() if err: cmd_res = err else: cmd_res = res.stdout.read() if not cmd_res: cmd_res = '執行成功'.encode('gbk') #解決粘包 length = len(cmd_res) data_length = struct.pack('i',length) conn.send(data_length) conn.send(cmd_res) except Exception as e: print(e) break conn.close() #客戶端 from socket import * ip_port = ('127.0.0.1',8000) buffer_size = 1024 backlog = 5 tcp_client = socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) while True: cmd = input('>>:').strip() if not cmd: continue if cmd == 'quit': break tcp_client.send(cmd.encode('utf-8')) #解決粘包 length = tcp_client.recv(4) length = struct.unpack('i',length)[0] recv_size = 0 recv_msg = b'' while recv_size < length: recv_msg += tcp_client.recv(buffer_size) recv_size = len(recv_msg) print(recv_msg.decode('gbk'))
開啓了服務器和一個客戶端以後,咱們在客戶端輸入一些命令,而後正確顯示,功能實現。這是在我再打開一個客戶端,輸入命令,發現服務器遲遲沒有響應。
這個就是當一個客戶端在請求的時候,當這個客戶端沒有結束的時候,服務器不會去處理其餘客戶端的請求。這時候就阻塞了。
如何讓服務器同時處理多個客戶端請求呢?
#服務端 import socketserver class Myserver(socketserver.BaseRequestHandler): """socketserver內置的通訊方法""" def handle(self): print('conn is:',self.request) #conn print('addr is:',self.client_address) #addr while True: try: #發消息 data = self.request.recv(1024) if not data:break print('收到的客戶端消息是:',data.decode('utf-8'),self.client_address) #發消息 self.request.sendall(data.upper()) except Exception as e: print(e) break if __name__ == '__main__': s = socketserver.ThreadingTCPServer(('127.0.0.1',8000), Myserver) #通訊循環 # s = socketserver.ForkingTCPServer(('127.0.0.1',8000), Myserver) #通訊循環 print(s.server_address) print(s.RequestHandlerClass) print(Myserver) print(s.socket) s.serve_forever() #客戶端 from socket import * ip_port = ('127.0.0.1',8000) buffer_size = 1024 backlog = 5 tcp_client = socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) while True: msg = input('>>:').strip() if not msg:continue if msg == 'quit':break tcp_client.send(msg.encode('utf-8')) data = tcp_client.recv(buffer_size) print(data.decode('utf-8')) tcp_client.close()
這段代碼經過socketserver模塊實現了socket的併發。這個過程當中,當一個客戶端在向服務器請求的時候,另外一個客戶端也能夠正常請求。服務器在處理一個客戶端請求的時候,另外一個請求沒有被阻塞。
總結:只要有一丁點阻塞,就是阻塞IO。
異步IO的特色就是全程無阻塞。
有些人常把同步阻塞和異步非阻塞聯繫起來,但實際上通過分析,阻塞與同步,非阻塞和異步的定義是不同的。同步和異步的區別是遇到IO請求是否等待。阻塞和非阻塞的區別是數據沒準備好的狀況下是否當即返回。同步多是阻塞的,也多是非阻塞的,而非阻塞的有多是同步的,也有多是異步的。
這裏面其實涉及到的知識點還不少,這裏只是憑個人記憶簡單總結了一下,之後會補充更多。