天天看點

Python進階知識點學習(九)

并發、并行,同步、異步,阻塞、非阻塞

并發、并行

  • 并發是在一個時間段内,有幾個程式在同一個cpu上運作,但是任意時刻隻有一個程式在cpu上運作。
  • 并行是任意時刻點上,有多個程式同時運作在多個cpu上。

同步、異步

  • 同步是指代碼調用IO操作時,必須等待IO操作完成才傳回的調用方式。
  • 異步是指代碼調用IO操作時,不必等待IO操作完成就傳回的調用方式。

阻塞、非阻塞

  • 阻塞是指調用函數時候目前線程被挂起。
  • 非阻塞是指調用函數時候目前線程不會被挂起,而是立即傳回。

阻塞和非阻塞的概念和同步異步感覺很像,但是其實它們之間是有差別的。

差別:

同步和異步實際上是消息通信的一種機制,可以把IO操作看做一個消息,調用IO操作時,相當于發一個消息給另外一個線程(或者說另外一個協程),讓它去執行某些操作,在送出資料之後立刻得到future,後邊就可以通過future拿到結果,實際上是消息之間的通信機制。

阻塞和非阻塞是不同于同步異步的,它是函數調的一種機制。

IO 多路複用 (select、poll 和 epoll)

unix中五種I/O模型

  1. 阻塞式I/O
  2. 非阻塞式I/O
  3. I/O複用
  4. 信号驅動式I/O (用的比較少)
  5. 異步I/O (POSIX的aio_系列函數)

以上五種是遞進式的發展。

I/O多路複用:

select方法也是阻塞的方法,select本身是阻塞式的,select可以監聽多個檔案句柄和socket,select在某一個檔案句柄或者socket準備好的話就會傳回,這時候立刻可以做業務邏輯處理。

I/O多路複用帶來的好處是:

比如現在同時發起了100個非阻塞式的請求,這時候直接使用select去監聽這100個socket,這樣的話一旦有一個發生狀态變化,我們就可以立馬處理它。

I/O多路複用中,将資料從核心複制到使用者空間這段時間消耗還是省不了。

異步IO:

這裡的異步IO是真正意義上的異步IO(aio),我們現在接觸到很多高并發架構實際上都沒有使用異步IO,實際上在很大程度上使用的都是io多路複用技術,IO多路複用很成熟很穩定,異步IO對于IO多路複用性能提升并沒有達到很明顯的程度,但是編碼難度有很大提升,是以目前情況下IO多路複用用的比較多。

異步IO節省掉了資料從核心拷貝到使用者空間這一步驟。

select、poll、epoll:

select、poll、epoll都是I/O多路複用的機制。

I/O多路複用就是通過一種機制,一個程序可以監視多個描述符,一旦某個描述符就緒(一般就是讀就緒或寫就緒),能夠通知程式進行相應的讀寫操作。

但select、poll、epoll本質上都是同步I/O,因為它們都需要在讀寫事件就緒後自己負責進行讀寫(資料從核心拷貝到使用者區),也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實作會負責把資料從核心拷貝到使用者空間。

select是什麼?

select 函數監視的檔案描述符分三類,分别是writefds、readfds、exceptfds。調用select後會阻塞,直到有描述符就緒(有資料可讀、可寫、或者有except),或者逾時(timeout指定等待時間,如果立即傳回設為null即可),函數傳回。當select函數傳回後,可以通過周遊fdset來找到就緒的描述符。

select目前幾乎在所有的平台上支援,其良好的跨平台也是它的一個優點。select的一個缺點在于單個程序監視的檔案描述符的數量有最大限制,在linux上一般為1024,可以通過修改宏定義甚至重新編譯核心的方式提升這一性質,但是這樣也會造成效率的降低。

poll是什麼?

不同于select使用三個位圖來表示三個fdset的方式,poll使用一個pollfd的指針實作。pollfd結構包含了要監視的event和發生的event,不再使用select "參數-值" 傳遞的方式。同時,pollfd并沒有最大數量限制(但是數量過大性能也會下降)。和select函數一樣,poll傳回後,需要輪詢pollfd來擷取就緒的描述符。

從上面看,select和poll都需要在傳回後,通過周遊檔案描述符來擷取已經就緒的socket。事實上,同時連接配接的大量用戶端在一時刻可能隻有很少的處于就緒的狀态,是以随着監視的描述符數量的增長,其效率也會線性下降。

epoll是什麼?

epoll是在2.6核心中提出的,epoll是之前的select和poll的增強版本。相對于select和poll來講,epoll更加靈活,沒有描述符限制。epoll使用一個檔案描述符管理多個描述符,将使用者關系的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy隻需要一次。

epoll它的查詢使用了資料結構中性能很高的一個:紅黑樹。

nginx就是使用了epoll。

epoll并不代表一定比select好:

  • 在并發高的情況下,連接配接活躍度不是很高, epoll比select。
  • 并發性不高,同時連接配接很活躍, select比epoll好。

非阻塞I/O實作http請求

上示例代碼:

import socket
from urllib.parse import urlparse
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")
           

非阻塞I/O整個過程依賴前後的監測,整個過程不停的做while循環檢測狀态,但是傳回時間沒有變,是以并沒有提高并發。

select+回調+事件循環實作http請求

目前開源的高性能架構,一般都是使用這種方式實作并發。

使用select + 回調 + 事件循環實作下載下傳網頁,并發性高且是單線程。

select方法本尊是在

import select

這個包裡邊,但是有另外一個包把select基礎上進行了封裝,用起來更簡單:

from selectors import DefaultSelector

,DefaultSelector一般使用DefaultSelector這個比較多。

看代碼示例:

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


selector = DefaultSelector()
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)

    # 當socket可讀時,讀資料,全部都是cpu操作
    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[:30])
            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():
    while not stop:
        ready = selector.select()
        for key, mask in ready:
            call_back = key.data
            call_back(key)

if __name__ == "__main__":
    # 計時開始
    start_time = time.time()
    for url in range(60):
        url = "http://www.baidu.com"
        urls.append(url)
        fetcher = Fetcher()
        fetcher.get_url(url)
    loop()
    print(time.time()-start_time)
           

上邊代碼中,Fetcher類包含三個方法,get_url履歷socket連接配接,connected和readable是兩個回調函數。

loop函數負責驅動整個事件循環。

回調的缺點

  1. 可讀性差
  2. 共享狀态異常處理
  3. 異常處理困難