Python 高階程式設計之IO模型與協程(四)

語言: CN / TW / HK

一、概述

接著上篇文章繼續,這篇文章主要講解IO模型和協程,這兩塊內容也是非常非常重要的。

Python的I/O模型分為同步(sync)非同步(async)兩種:

  • 同步I/O模型是指,當一個執行緒在等待I/O操作完成時,它不能執行其他任務,需要一直等待I/O操作完成,直到接收到I/O操作的完成通知後才繼續執行。

  • 非同步I/O模型是指,當一個執行緒發起一個I/O操作後,不會等待I/O操作完成,而是直接執行其他任務,當I/O操作完成後,再通過回撥或事件通知來處理I/O操作的結果。

Python 中的協程:

  • 協程是一種輕量級的使用者級執行緒,它在單執行緒內執行不會阻塞主執行緒,可以在多個任務間輕鬆地切換,因此可以用於實現非同步I/O操作。協程的實現方式與生成器非常相似,通過使用yield語句來暫停和恢復執行
  • 協程可以與asyncio庫配合使用,來實現非同步I/O操作。這種方式可以極大地提高程式的效率,因為程式不必等待I/O操作完成,可以在等待I/O操作期間執行其他任務。

在這裡插入圖片描述

二、IO模型

上面已經對IO模型大致描述了一下,其實細分可以分為五種模型:

  • 同步阻塞IO(Blocking IO):即傳統的IO模型。
  • 同步非阻塞IO(Non-blocking IO):預設建立的socket都是阻塞的,非阻塞IO要求socket被設定為NONBLOCK。
  • IO多路複用(IO MulTIplexing):即經典的Reactor設計模式,有時也稱為非同步阻塞IO,Java中的Selector和Linux中的epoll都是這種模型。
  • 非同步IO(Asynchronous IO):即經典的Proactor設計模式,也稱為非同步非阻塞IO。
  • signal driven IO:訊號驅動IO , 在實際中並不常用,所以只剩下四種IO Model。

1)IO 模型準備

IO模型講解之前,先了解一些概念:

  • 使用者空間和核心空間
  • 程序切換
  • 程序的阻塞
  • 檔案描述符
  • 快取 I/O

1、使用者空間和核心空間

使用者空間和核心空間是作業系統的兩個重要概念。

  • 使用者空間是一個獨立的記憶體空間,其中儲存著使用者程序(包括應用程式)執行時使用的資料和程式碼。使用者空間是安全的,因為程序之間相互獨立,不能直接訪問對方的記憶體空間。

  • 核心空間作業系統核心程式碼和資料結構所使用的記憶體空間。核心空間是不安全的,因為核心程式碼執行時有最高的許可權,可以訪問系統的所有資源,並且可以直接控制其他所有程序。

  • 使用者空間和核心空間之間的分界線是很重要的,因為它保護了作業系統的安全和穩定。例如,如果一個使用者程序的程式碼出現問題,它可能會崩潰,但是它不會影響整個系統的穩定性。

2、程序切換

程序切換是作業系統的一個基本概念,指的是作業系統在不同的程序之間進行切換,以便實現多工處理。

程序切換的過程包括如下步驟:

  • 儲存當前程序的狀態:在程序切換前,作業系統需要儲存當前程序的執行狀態,包括 CPU 暫存器的值、棧的內容等。

  • 選擇下一個要執行的程序:作業系統根據排程演算法選擇下一個要執行的程序,例如先來先服務(FCFS)、最短程序優先(SPN)等。

  • 載入下一個程序的狀態:作業系統從儲存器中讀取下一個程序的狀態,並載入到 CPU 暫存器中。

  • 恢復下一個程序的執行:作業系統將 CPU 控制權交給下一個程序,讓它繼續執行。

程序切換是一個高代價的操作,需要大量的時間和系統資源。因此,作業系統需要合理地排程程序,以最小化程序切換的次數。

3、程序的阻塞

程序阻塞是指一個程序因為等待某個事件的發生而暫時停止執行。這個事件可以是等待 I/O 操作完成、等待資源的分配、等待訊號的到達等。當事件發生時,該程序再恢復執行。

  • 程序阻塞是一種常見的程序狀態,與其他程序狀態(例如就緒狀態、執行狀態)不同。當一個程序阻塞時,它不再需要 CPU 的執行,因此作業系統可以切換到其他程序上,以利用 CPU 資源。

  • 程序阻塞可以減少 CPU 的空閒時間,並有助於有效地管理系統資源,但是需要消耗大量的系統資源來維護阻塞佇列和狀態資訊。因此,作業系統需要合理地管理程序阻塞,以保證系統的高效執行。

4、檔案描述符fd

檔案描述符(file descriptor)是一個非負整數,用於指代一個開啟的檔案或其他 I/O 裝置(例如管道、套接字等)。它為程式提供了一種用於訪問檔案或 I/O 裝置的抽象方法,而不需要知道底層的實現細節。

  • 每個程序都有一個檔案描述符表,該表包含了每個開啟的檔案或 I/O 裝置的資訊。當程式開啟一個檔案或 I/O 裝置時,作業系統會分配一個檔案描述符,並將其儲存在該程序的檔案描述符表中。之後,程式可以使用該檔案描述符來讀寫檔案或控制 I/O 裝置。
  • 檔案描述符是系統資源,需要恰當地使用和管理。當一個程序結束時,該程序的所有開啟的檔案和 I/O 裝置都將關閉,相應的檔案描述符也將被釋放。因此,程式必須確保在不再使用檔案描述符時關閉它。

5、快取 I/O

  • 快取 I/O 指的是使用快取(即臨時儲存區)來儲存經常訪問的資料。在計算機技術中,這可以指的是將資料快取在系統的記憶體或儲存裝置中,或者使用網路中的快取(如網頁瀏覽器快取或代理快取)來減少需要通過網路傳輸的資料量。
  • 快取可以通過減少執行的 I/O(輸入/輸出)操作來提高系統的效能,因為資料可以從快取中快速獲取,而不是每次都從原始來源中獲取。這可以導致更快的訪問時間和減少的延遲,從而提高整體系統效能。

2)IO 模型詳解

1、同步阻塞IO(Blocking IO)

Python 中的同步阻塞 I/O 是一種 I/O 操作,在這種情況下,程式的執行會被阻塞,直到 I/O 操作完成。換句話說,程式將等待 I/O 操作完成,才能繼續執行下一個任務。這種型別的 I/O 被稱為“阻塞”,因為它阻塞了程式的執行,並且被稱為“同步”,因為它以同步的方式發生,程式等待 I/O 操作完成後再繼續。

  • 例如,當使用 Python 中的 read 方法從檔案讀取時,程式將等待整個檔案讀取完畢,才能繼續執行下一個任務。這是同步阻塞 I/O 的一個例子。

  • 當 I/O 操作相對較短且程式可以等待其完成後才繼續執行下一個任務時,通常會使用同步阻塞 I/O。然而,當 I/O 操作比較長時,程式可能會被阻塞很長一段時間,從而導致效能下降。在這種情況下,通常更好使用非同步 I/O。

  • linux中,預設情況下所有的socket都是阻塞IO,一個典型的讀操作流程大概是這樣:

img

我們之前寫的都是阻塞IO模型(協程除外)

在服務端開設多程序或者多執行緒 程序池執行緒池 其實還是沒有解決IO問題 該等的地方還是得等 沒有規避 只不過多個人等待彼此互不干擾。示例如下:

服務端:

```python import socket

server = socket.socket() server.bind(('127.0.0.1',8080)) server.listen(5)

while True: conn, addr = server.accept() while True: try: data = conn.recv(1024) if len(data) == 0:break print(data) conn.send(data.upper()) except ConnectionResetError as e: break conn.close() ```

客戶端:

```python import socket

client = socket.socket() client.connect(('127.0.0.1',8081))

while True: client.send(b'hello world') data = client.recv(1024) print(data) ```

2、同步非阻塞IO(Non-blocking IO)

Python 中的同步非阻塞 I/O 是一種程式不必等待 I/O 操作完成就可以繼續執行下一個任務的 I/O 操作。相反,程式在 I/O 操作正在進行時繼續執行下一個任務。這種型別的 I/O 被稱為“非阻塞”,因為它不會阻塞程式的執行,並且被稱為“同步”,因為它仍以同步的方式操作,程式會定期檢查 I/O 操作的狀態。

  • 例如,在 Python 中實現同步非阻塞 I/O,可以使用 select 模組,該模組提供了對作業系統底層 I/O 多路複用功能的訪問,允許您同時監視多個 I/O 操作,而不會阻塞。

  • 當 I/O 操作預計需要很長時間才能完成,且程式需要在此期間繼續處理其他任務時,通常使用同步非阻塞 I/O。這可以提高程式的效能和響應性。

  • python下,可以通過設定socket使其變為non-blocking(server.setblocking(False))。當對一個non-blocking socket執行讀操作時,流程是這個樣子:

img

示例如下:

服務端:

```python import socket import time

server = socket.socket() server.bind(('127.0.0.1', 8081)) server.listen(5) server.setblocking(False)

將所有的網路阻塞變為非阻塞

r_list = [] del_list = [] while True: try: conn, addr = server.accept() r_list.append(conn) except BlockingIOError: for conn in r_list: try: data = conn.recv(1024) # 沒有訊息 報錯 if len(data) == 0: # 客戶端斷開連結 conn.close() # 關閉conn # 將無用的conn從r_list刪除 del_list.append(conn) continue conn.send(data.upper()) except BlockingIOError: continue except ConnectionResetError: conn.close() del_list.append(conn) # 揮手無用的連結 for conn in del_list: r_list.remove(conn) del_list.clear() ```

客戶端:

```python import socket

client = socket.socket() client.connect(('127.0.0.1',8081))

while True: client.send(b'hello world') data = client.recv(1024) print(data) ```

3、IO多路複用(IO MulTIplexing)

"IO 多路複用" 是一種用於監聽多個網路連線的技術,以提高網路程式的效能和效率。常用的 IO 多路複用技術有 select、poll、epoll

在 Python 中,可以使用 select 模組來實現 IO 多路複用。select 模組提供了三個函式:select、poll、epoll,可以在不同的平臺上使用不同的函式來實現 IO 多路複用。它們都是用來監聽多個檔案描述符(socket)的讀寫情況,以實現對多個socket的高效管理。

  • select:select 是最早的 I/O 多路複用 API 之一,並在大多數 Unix 類系統上廣泛支援。select 可以監視大量的檔案描述符,但它有許多限制,例如具有固定的最大檔案描述符數量(1024),以及當正在監視的檔案描述符列表更改時受到競爭條件的影響。

  • poll:poll 是為了解決 select 中的一些限制而引入的,在大多數 Unix 類系統上廣泛支援。poll 可以監視比 select 更多的檔案描述符(無限制),並且它還提供了關於每個檔案描述符狀態的更多資訊。

  • epoll:epoll 被引入為 poll 和 select 的更有效替代品,並可在 Linux 系統上使用。epoll 具有許多比 poll 和 select 更有效的效能優勢,例如更快更可擴充套件的設計,以及能夠以較低開銷監視大量檔案描述符的能力。

select、poll、epoll之間的區別:

| | select | poll | epoll | |--|--|--|--| | 操作方式 | 遍歷 | 遍歷 | 回撥 | | 底層實現 | 陣列 | 連結串列 | 雜湊表 | | IO效率 | 每次呼叫都進行線性遍歷,時間複雜度為O(n) | 每次呼叫都進行線性遍歷,時間複雜度為O(n) | 事件通知方式,每當fd就緒,系統註冊的回撥函式就會被呼叫,將就緒fd放到rdllist裡面。時間複雜度O(1) | | 最大連線數 | 1024(x86)或 2048(x64) | 無上限 | 無上限 | | fd拷貝 | 每次呼叫select,都需要把fd集合從使用者態拷貝到核心態 | 每次呼叫poll,都需要把fd集合從使用者態拷貝到核心態 | 呼叫epoll_ctl時拷貝進核心並儲存,之後每次epoll_wait不拷貝 |

它的流程如圖:

img

  • 管的物件只有一個的時候 其實IO多路複用連阻塞IO都比不上!!!但是IO多路複用可以一次性監管很多個物件
  • 監管機制是作業系統本身就有的 如果你想要用該監管機制(select)需要,
  • 你匯入對應的select模組

示例如下:

```python import socket import select

server = socket.socket() server.bind(('127.0.0.1',8080)) server.listen(5) server.setblocking(False) read_list = [server]

while True: r_list, w_list, x_list = select.select(read_list, [], []) """ 幫你監管 一旦有人來了 立刻給你返回對應的監管物件 """ # print(res) # ([], [], []) # print(server) # print(r_list) for i in r_list: # """針對不同的物件做不同的處理""" if i is server: conn, addr = i.accept() # 也應該新增到監管的佇列中 read_list.append(conn) else: res = i.recv(1024) if len(res) == 0: i.close() # 將無效的監管物件 移除 read_list.remove(i) continue print(res) i.send(b'hello python')

# 客戶端 import socket

client = socket.socket() client.connect(('127.0.0.1',8080))

while True:

client.send(b'hello world')
data = client.recv(1024)
print(data)

```

4、非同步IO(Asynchronous IO)

非同步IO模型是所有模型中效率最高的 也是使用最廣泛的 。先看一下它的流程:

img

```python """ 非同步IO模型是所有模型中效率最高的 也是使用最廣泛的 相關的模組和框架 模組: asyncio模組 非同步框架:sanic tronado twisted 速度快!!! """ import threading import asyncio

@asyncio.coroutine def hello(): print('hello world %s'%threading.current_thread()) yield from asyncio.sleep(1) # 換成真正的IO操作 print('hello world %s' % threading.current_thread())

loop = asyncio.get_event_loop() tasks = [hello(),hello()] loop.run_until_complete(asyncio.wait(tasks)) loop.close() ```

三、Python 協程介紹

"協程"是一種在單執行緒內實現多工的機制,它的目的是讓程式設計師可以方便地實現非同步 I/O 操作,這是通過在單執行緒中進行切換任務完成的。

  • 在 Python 中,協程是通過生成器實現的。它們與普通生成器有所不同,因為它們需要使用 async/await 語法以及 asyncio 庫來工作。

  • 使用協程可以簡化非同步程式設計,因為您可以使用類似同步程式碼的方式來編寫非同步程式碼。它們也可以在單執行緒中有效地利用 CPU 時間,因為它們可以在沒有阻塞的情況下等待 I/O 操作的完成。

  • 舉個例子,假設您有一個網路伺服器,需要同時處理多個客戶端請求。使用協程,您可以在單執行緒中為每個客戶端建立一個協程,並在客戶端請求完成後從協程中恢復,從而接受其他客戶端的請求。

  • 總的來說,協程是一種高效且簡潔的非同步程式設計方法,為您的 Python 程式提供了更多的併發性和效率。

以下是一個簡單的 Python 協程示例程式碼:

```python import asyncio

async def coroutine_example(): print("This is a coroutine example") await asyncio.sleep(1) print("Coroutine example is done!")

async def main(): task = asyncio.create_task(coroutine_example()) await task

asyncio.run(main()) ``` 該程式碼定義了一個名為 coroutine_example 的協程,該協程列印一條訊息,然後使用 asyncio.sleep 函式暫停 1 秒。隨後,程式碼定義了一個名為 main 的函式,該函式建立了一個任務,並使用 await task 等待該任務完成。最後,程式碼使用 asyncio.run 函式執行 main 函式,並啟動協程。

執行該程式碼後,您將看到以下輸出:

python This is a coroutine example Coroutine example is done!

四、程序、執行緒與協程的關係與區別

程序、執行緒和協程是作業系統中用來管理程式執行的三種不同的技術。

  • 程序:程序是操作系統中最基本的資源分配單元,是程式的實體。它是系統進行資源分配和排程的獨立單位。每個程序都有獨立的記憶體空間,因此在一個程序中出現故障不會影響其他程序。

  • 執行緒:執行緒是程序的一個執行單元,是作業系統分配資源的最小單元。多執行緒可以共享程序的資源,因此執行緒間的通訊和協作比程序間更加方便。不幸的是,執行緒之間存在競爭關係,並且當一個執行緒出現故障時,整個程序都會受到影響。

  • 協程:協程是一種在單執行緒中執行多工的機制,是執行緒的一種特殊形式。它不同於多執行緒,因為它不會獨立分配資源,而是在一個執行緒中共享資源。因此,協程的實現比執行緒更加輕量,可以提高程式的效率。不過,協程的實現也比執行緒更加複雜,因為需要編寫更多的程式碼來協調任務的。

這裡只是簡單的講了一下協程的一些概念和簡單示例,下篇文章會重點講解使用生成器實現協程,IO模型和協程是python非常重要的兩個知識點,也是提升python程式碼執行效率的最有校方法和思路。小夥伴可以關注我的公眾號【大資料與原生技術分享】進行深入技術交流~