經常會有客戶提問,服務器上如果使用了點量實時云渲染(也叫像素流或者云流化)技術服務,可以支持多少路并發?這個問題其實之前我們有簡單說過,影響并發的兩個因素:程序本身的情況以及服務器的參數性能,具體可參考文章《虛幻4像素流送技術支持多少人并發?》。但點量小芹發現,很多時候通過這個方法還是無法判斷,今天我們就來說個一個簡單的方法,供大家參考,具體可以根據情況自己評估下。好了不多說直接上干貨。
點量實時云渲染軟件,支持服務器開啟多少路并發判斷方法如下:
1、找一臺服務器安裝需要云流化的內容,比如UE4或者Unity3D的EXE程序(也可以是其他的Windows下的EXE程序),注意為了更好的測試,可以復制到多個文件夾。
2、一次次打開安裝的EXE程序,最好進入程序中消耗資源比較大的界面,同時觀察CPU和GPU的負載,在二者達到85%左右的時候,看看打開了多少個EXE程序。一般這就是這臺服務器上能同時開啟的并發路數。
從以上我們可以看出,具體能支持多少并發,其實和云渲染技術或者軟件沒有太大關系,主要還是取決于程序本身的情況以及所選服務器的性能。這里小芹在舉個例子,假設有個unity3D的exe程序,在i5處理器、1050顯卡可以流暢跑一路,如果服務器有更多核心、更好的CPU,使用多張3070Ti之類的高性能顯卡,則有可能支持8~12路的并發。
實時云渲染軟件
那么使用了實時云渲染技術(也叫像素流或者云流化)支持了10路并發只能10個人用嗎?如果是10個人同時操作,則確實只能10個人用。超過這個數量可以設置其他人圍觀或者排隊等待,但同時只能10個人操作。那圍觀的人數多了會有什么影響呢?這個其實和看視頻是一個道理,人數多了之后帶寬的要求就高了,假設一路并發的碼率設置是8M,10路并發需要80M ,如果同時還有20人圍觀,則需要的總帶寬數是8*30=240M。那要是帶寬不足了怎么辦呢?有兩種辦法,不做人數限制,這種情況下會出現操作不流暢卡頓的情況。另一種方法就是達到帶寬的上線后,不再允許新人員進入系統,給一個等待的提示。
預計更新
1. Python 簡介
- Python 簡介和歷史
- Python 特點和優勢
- 安裝 Python
2. 變量和數據類型
- 變量和標識符
- 基本數據類型:數字、字符串、布爾值等
- 字符串操作
- 列表、元組和字典
3. 控制語句和函數
- 分支結構:if/else 語句
- 循環結構:for 和 while 循環
- 函數
- 參數傳遞與返回值
- Lambda 表達式
4. 模塊和文件 IO
- 模塊的概念
- 導入模塊
- 文件 IO
- 序列化和反序列化
5. 異常處理
- 異常簡介
- try/except 語句
- 自定義異常
6. 面向對象編程
- 類和對象
- 繼承和多態
- 屬性和方法
- 抽象類和接口
7. 正則表達式
- 正則表達式概述
- 匹配和搜索
- 替換和分割
8. 并發編程
- 多線程
- 多進程
- 協程和異步編程
9. 數據庫編程
- 關系型數據庫介紹
- 使用 SQLite 數據庫
- 使用 MySQL 數據庫
- 使用 PostgreSQL 數據庫
10. 網絡編程
- Socket 編程簡介
- TCP Socket 編程
- UDP Socket 編程
- HTTP 編程
11. Web 開發框架 Flask
- Flask 簡介
- 安裝 Flask
- 路由和視圖函數
- 模板和靜態文件
12. 數據分析和科學計算
- NumPy 基礎
- Pandas 基礎
- Matplotlib 基礎
13. 機器學習入門
- 機器學習概述
- 監督學習和非監督學習
- Scikit-Learn 簡介
- 利用 Scikit-Learn 進行數據預處理和模型訓練
14. 自然語言處理
- 自然語言處理概述
- 中文分詞和處理
- 文本分類和情感分析
15. 游戲開發與 Pygame
- Pygame 簡介
- Pygame 基礎
- 開發一個簡單的游戲
8. 并發編程
- 多線程
- 多進程
- 協程和異步編程
Python多線程是指使用Python編寫并發程序時,通過創建多個線程來提高程序的執行效率。多線程可以讓程序在同一時間內同時處理多個任務,從而提高程序的運行速度和響應能力。在Python中,多線程的實現主要依賴于threading模塊。
1. 線程和進程的區別
在開始討論Python多線程之前,需要先了解線程和進程的概念以及它們之間的區別。
進程是操作系統資源分配的基本單位,每個進程都有自己獨立的地址空間,并占用著一定的系統資源(如CPU、內存等)。進程與進程之間是相互獨立的,一個進程崩潰或者被殺死不會影響到其他進程。
線程是進程中的執行單元,一個進程可以包含多個線程。線程之間共享進程資源,每個線程有自己的棧和局部變量,但是它們共享全局變量、靜態變量等。不同線程之間切換的開銷比進程之間切換的開銷小得多。
2. Python threading模塊
Python提供了threading模塊來支持多線程編程。該模塊提供了Thread類來創建線程,常用的方法有:
`Thread(target=, args=)`:創建新的線程。
`start()`:啟動線程。
`join([time])`:等待線程運行結束。
`is_alive()`:判斷線程是否在運行。
3. 創建線程
創建一個新的線程需要使用Thread類,它的構造函數如下:
```
Thread(target=, args=(), name=)
```
其中,`target`參數為該線程所要執行的目標函數。如果不指定`name`,則每個線程會自動生成一個唯一的名字。
示例代碼如下:
```python
import threading
import time
# 定義線程處理函數
def thread_func(thread_id):
print('Thread %d is running...' % thread_id)
time.sleep(2) # 模擬線程執行時間
print('Thread %d is done.' % thread_id)
# 創建5個線程并啟動
for i in range(5):
t=threading.Thread(target=thread_func, args=(i,))
t.start()
# 等待所有線程運行結束
for t in threading.enumerate():
if t !=threading.current_thread():
t.join()
```
上面的代碼中,我們首先定義了一個線程處理函數`thread_func`,它接受一個參數`thread_id`,用于標識當前線程。然后我們使用`threading.Thread`類創建了5個線程,并分別傳遞給它們不同的`thread_id`參數。
通過調用`start()`方法來啟動線程,這將會調用線程處理函數。主線程繼續往下執行,而新產生的線程在后臺運行。
最后,我們使用`enumerate()`方法獲取所有的線程,并調用`join()`方法等待它們運行結束。
4. 線程同步
在多線程編程中,線程之間會共享一些數據,如果多個線程同時修改同一個變量可能會導致不可預期的結果。所以需要對共享資源進行同步處理。
Python提供了Lock、RLock、Semaphore、Event、Condition等同步機制來實現線程同步。其中,Lock和RLock都是互斥鎖,只允許一個線程訪問被保護的共享資源;Semaphore是信號量,允許多個線程同時訪問某個資源;Event可以用于線程之間通信,一個線程可以通過set()方法發出事件,其他線程可以通過wait()方法等待該事件的發生;Condition可以用于控制線程執行的順序,它可以讓某些線程等待特定條件的發生,再繼續執行。
下面我們分別介紹這些同步機制的使用方法。
4.1 Lock
Lock是最簡單也是最常用的同步機制,它的作用是保證對共享資源的互斥訪問。
在Python中,Lock可以通過threading模塊來創建:
```python
lock=threading.Lock()
```
然后在需要保護的代碼塊前后加上acquire()和release()方法,如下所示:
```python
import threading
# 共享變量
count=0
# 創建鎖
lock=threading.Lock()
# 線程處理函數
def thread_func():
global count, lock
with lock:
for i in range(100000):
count +=1
# 創建10個線程并啟動
threads=[]
for i in range(10):
t=threading.Thread(target=thread_func)
threads.append(t)
t.start()
# 等待所有線程運行結束
for t in threads:
t.join()
# 輸出count的值
print('count=%d' % count)
```
上面的代碼中,我們先定義了一個全局變量`count`,然后使用`threading.Lock()`創建了一個鎖對象`lock`。在線程處理函數中,我們通過`with lock:`語句獲取鎖,然后對`count`進行累加操作。
由于多個線程會同時競爭同一個鎖,所以只有一個線程能夠獲得鎖,并執行累加操作。其他線程則會阻塞在`with lock:`語句處,等待鎖的釋放。
最后,我們通過輸出`count`的值檢查程序的正確性。
4.2 RLock
RLock是可重入鎖,它允許同一個線程多次獲取鎖。這對于一些需要遞歸調用的場景非常有用。其使用方法與Lock類似,唯一的區別就是可以多次acquire()。
```python
import threading
# 共享變量
count=0
# 創建鎖
lock=threading.RLock()
# 線程處理函數
def thread_func():
global count, lock
with lock:
with lock: # 多次acquire()
for i in range(100000):
count +=1
# 創建10個線程并啟動
threads=[]
for i in range(10):
t=threading.Thread(target=thread_func)
threads.append(t)
t.start()
# 等待所有線程運行結束
for t in threads:
t.join()
# 輸出count的值
print('count=%d' % count)
```
上面的代碼中,我們創建了一個RLock對象`lock`,然后在線程處理函數中使用多次`with lock:`語句來演示RLock的使用方法。
4.3 Semaphore
Semaphore是信號量,它控制對共享資源的訪問數量。當一個線程獲得了信號量之后,其他線程必須等待該線程釋放信號量后才能繼續執行。
在Python中,Semaphore可以通過threading模塊來創建:
```python
semaphore=threading.Semaphore(value)
```
其中`value`參數表示信號量的初始值,默認為1。然后我們可以使用acquire()和release()方法來獲取和釋放信號量。
```python
import threading
# 共享變量
count=0
# 創建信號量
semaphore=threading.Semaphore(value=5)
# 線程處理函數
def thread_func():
global count, semaphore
with semaphore: # 獲取信號量
for i in range(100000):
count +=1
semaphore.release() # 釋放信號量
# 創建10個線程并啟動
threads=[]
for i in range(10):
t=threading.Thread(target=thread_func)
threads.append(t)
t.start()
# 等待所有線程運行結束
for t in threads:
t.join()
# 輸出count的值print('count=%d' % count)
上面的代碼中,我們先創建了一個Semaphore對象`semaphore`,將其初始值設為5。然后在線程處理函數中使用`with semaphore:`語句來獲取信號量,執行累加操作后再釋放信號量。
由于Semaphore的初始值為5,所以最多只有5個線程可以同時執行累加操作,其他線程必須等待信號量的釋放才能繼續執行。
4.4 Event
Event是用于線程之間通信的同步機制,它允許一個線程發出事件,其他線程等待該事件的發生。在Python中,Event可以通過threading模塊來創建:
```python
evt=threading.Event()
```
然后我們可以使用set()方法發出事件,使用wait()方法等待事件的發生。
```python
import threading
# 創建事件
evt=threading.Event()
# 線程1處理函數
def thread_func1():
print('Thread 1 is waiting...')
evt.wait() # 等待事件發生
print('Thread 1 is done.')
# 線程2處理函數
def thread_func2():
print('Thread 2 is running...')
for i in range(5):
print('Thread 2 is working...')
time.sleep(1)
evt.set() # 發出事件
print('Thread 2 is done.')
# 創建線程并啟動
t1=threading.Thread(target=thread_func1)
t2=threading.Thread(target=thread_func2)
t1.start()
t2.start()
# 等待所有線程運行結束
t1.join()
t2.join()
```
上面的代碼中,我們創建了一個Event對象`evt`。在線程1處理函數中,我們使用`evt.wait()`語句等待事件發生;在線程2處理函數中,我們使用`evt.set()`方法發出事件。
由于線程1等待事件的發生,所以會一直阻塞在`evt.wait()`語句處,直到線程2發出事件才能繼續執行。
4.5 Condition
Condition是控制線程執行順序的同步機制,它可以讓某些線程等待特定條件的發生,再繼續執行。
在Python中,Condition可以通過threading模塊來創建:
```python
cond=threading.Condition(lock=None)
```
其中`lock`參數表示該Condition使用的鎖,默認為RLock。
然后我們可以使用wait()、notify()和notify_all()方法來控制線程之間的執行順序。
```python
import threading
# 共享變量
count=0
# 創建條件變量
cond=threading.Condition()
# 線程1處理函數
def thread_func1():
global count, cond
with cond:
while count < 5:
print('Thread 1 is waiting...')
cond.wait() # 等待條件變量
print('Thread 1 is done.')
# 線程2處理函數
def thread_func2():
global count, cond
with cond:
for i in range(5):
count +=1
print('Thread 2 is working...')
time.sleep(1)
cond.notify() # 通知條件變量已經滿足
print('Thread 2 is done.')
# 創建線程并啟動
t1=threading.Thread(target=thread_func1)
t2=threading.Thread(target=thread_func2)
t1.start()
t2.start()
# 等待所有線程運行結束
t1.join()
t2.join()
```
上面的代碼中,我們創建了一個Condition對象`cond`。在線程1處理函數中,我們使用`while count < 5:`語句等待條件變量;在線程2處理函數中,我們通過累加操作使得條件變量滿足,并使用`cond.notify()`方法通知等待的線程。
由于線程1等待條件變量的發生,所以會一直阻塞在`cond.wait()`語句處,直到線程2發出通知才能繼續執行。
5. 線程安全問題
多線程編程中最常見的問題就是線程安全問題。由于多個線程可能同時訪問共享資源,所以如果不加以保護,就會出現數據不一致等問題。
在Python中,我們可以使用鎖、信號量、條件變量等同步機制來解決線程安全問題。具體來說,我們可以按照以下步驟來實現線程安全:
1. 定義共享資源并初始化。
2. 創建互斥鎖或信號量等同步機制。
3. 在對共享資源進行操作前獲取鎖或信號量。
4. 對共享資源進行操作。
5. 釋放鎖或信號量。
例如,在第4節中,我們使用鎖和信號量等同步機制來保護了`count`這個共享變量,避免了多個線程同時修改它而引發的線程安全問題。
另外,為了避免死鎖問題,我們還需要注意同步機制的使用方法。一般來說,當多個線程需要獲取多個鎖時,我們應該按照固定的順序獲取鎖,避免死鎖的發生。
6. 總結
本文介紹了Python中的多線程編程,并詳細講解了線程的創建、啟動、結束等基本操作,以及同步機制的使用方法。同時,我們還介紹了線程安全問題的解決方法,包括鎖、信號量、條件變量等同步機制的使用。
最后,需要注意的是,在進行多線程編程時,我們應該盡可能地避免共享資源的使用,盡量使用局部變量等本地數據,從而減少線程之間的競爭和沖突。
一、概述
多進程是指在操作系統中,同時運行多個進程,每個進程都有自己的獨立地址空間,它們之間相互獨立,互不影響。多進程編程可以充分利用多核CPU的性能優勢,提高程序的執行效率。
Python標準庫中提供了`multiprocessing`模塊,用于支持多進程編程。該模塊提供了Process、Queue、Pipe等類和函數,方便我們創建和管理子進程,以及進行進程間通信。
本文將從以下幾個方面介紹Python多進程編程:
1. 進程的基本概念
2. 創建和啟動進程
3. 進程間通信
4. 進程池
5. 多進程編程實踐
二、進程的基本概念
進程是操作系統中的一個基本概念,它表示正在執行的程序。每個進程都有自己的獨立地址空間,包括代碼段、數據段、堆棧等。進程之間相互獨立,互不干擾。
在Linux系統中,我們可以使用`ps`命令查看當前系統中正在運行的所有進程:
```
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.2 19060 2520 ? Ss Mar22 0:03 /sbin/init
root 2 0.0 0.0 0 0 ? S Mar22 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? I< Mar22 0:00 [rcu_gp]
root 4 0.0 0.0 0 0 ? I< Mar22 0:00 [rcu_par_gp]
...
```
其中,PID表示進程的ID,COMMAND表示進程的命令。
在Python中,我們可以使用`os`模塊來獲取當前進程的ID和父進程的ID:
```python
import os
print('Current process ID:', os.getpid())
print('Parent process ID:', os.getppid())
```
輸出結果如下:
```
Current process ID: 12345
Parent process ID: 67890
```
三、創建和啟動進程
在Python中,我們可以通過`multiprocessing`模塊來創建和啟動子進程。`multiprocessing`模塊提供了Process類,用于表示一個進程對象。
下面是一個簡單的例子,演示如何使用Process類創建和啟動子進程:
```python
from multiprocessing import Process
import os
# 子進程執行的代碼
def child_proc(name):
print('Child process %s (%s) running...' % (name, os.getpid()))
if __name__=='__main__':
# 創建子進程并啟動
p=Process(target=child_proc, args=('test',))
print('Parent process %s.' % os.getpid())
p.start()
p.join()
print('Child process end.')
```
上面的代碼中,我們首先定義了一個函數`child_proc`,用于表示子進程要執行的代碼。然后,在主進程中,我們使用Process類創建了一個子進程對象`p`,并通過`start()`方法啟動子進程。
注意到我們在創建子進程之前添加了一句`if __name__=='__main__':`,這是因為在Windows系統中,由于多進程模塊會將整個程序復制一份作為新的進程運行,如果沒有加上這句判斷語句,就會導致無限遞歸。
輸出結果如下:
```
Parent process 12345.
Child process test (67890) running...
Child process end.
```
從輸出結果可以看出,子進程已經成功創建并運行,打印出了子進程的ID和父進程的ID。
四、進程間通信
在多進程編程中,由于每個進程都有自己獨立的地址空間,所以它們之間無法直接訪問彼此的變量。為了實現進程間的數據共享和通信,我們需要使用進程間通信(IPC)機制。
`multiprocessing`模塊提供了多種進程間通信方式,包括隊列、管道、共享內存等。這里我們主要介紹Queue和Pipe兩種方式。
4.1 Queue
Queue是一種線程安全的隊列,可以用于多進程之間的通信。在Python中,我們可以使用`multiprocessing.Queue`類來創建一個隊列對象。
下面是一個簡單的例子,演示如何使用Queue進行進程間通信:
```python
from multiprocessing import Process, Queue
# 子進程執行的代碼
def child_proc(q):
value=q.get() # 從隊列中獲取數據
print('Child process received:', value)
if __name__=='__main__':
# 創建隊列并傳遞給子進程
q=Queue()
p=Process(target=child_proc, args=(q,))
# 往隊列中放入數據
q.put('Hello, world!')
# 啟動子進程
p.start()
p.join()
```
上面的代碼中,我們首先創建了一個Queue對象`q`,然后通過Process類創建了一個子進程對象`p`。在主進程中,我們使用`q.put()`方法往隊列中放入了一條數據。
在子進程中,我們使用`q.get()`方法獲取隊列中的數據,并打印出來。
輸出結果如下:
```
Child process received: Hello, world!
```
可以看到,子進程成功地從隊列中取出了數據并打印出來。
4.2 Pipe
Pipe是一種雙向管道,可以用于兩個進程之間的通信。在Python中,我們可以使用`multiprocessing.Pipe`類來創建一個管道對象。
下面是一個簡單的例子,演示如何使用Pipe進行進程間通信:
```python
from multiprocessing import Process, Pipe
# 子進程執行的代碼
def child_proc(conn):
data=conn.recv() # 接收主進程發送的數據
print('Child process received:', data)
conn.send('Hello, world!') # 向主進程發送數據
if __name__=='__main__':
# 創建管道并傳遞給子進程
parent_conn, child_conn=Pipe()
p=Process(target=child_proc, args=(child_conn,))
# 向子進程發送數據
parent_conn.send('Hello, child!')
# 啟動子進程
p.start()
# 接收子進程返回的數據
data=parent_conn.recv()
print('Parent process received:', data)
p.join()
```
上面的代碼中,我們首先使用`multiprocessing.Pipe()`函數創建了一對管道對象`parent_conn`和`child_conn`,然后通過Process類創建了一個子進程對象`p`。在主進程中,我們使用`parent_conn.send()`方法向子進程發送了一條數據。
在子進程中,我們使用`conn.recv()`方法接收主進程發送的數據,并使用`conn.send()`方法向主進程發送了一條數據。
在主進程中,我們使用`parent_conn.recv()`方法接收子進程返回的數據,并打印出來。
輸出結果如下:
```
Child process received: Hello, child!
Parent process received: Hello, world!
```
可以看到,主進程成功地向子進程發送了一條數據,并從子進程接收了一條數據。
五、進程池
在多進程編程中,為避免頻繁創建和銷毀進程帶來的系統開銷,我們可以使用進程池(Pool)來管理進程,以便重復利用已創建的進程。
`multiprocessing`模塊提供了Pool類,用于創建一個進程池對象。我們可以通過調用該對象的`apply()`、`apply_async()`、`map()`、`imap()`等方法來提交任務,并返回結果。
下面是一個簡單的例子,演示如何使用進程池執行任務:
```python
from multiprocessing import Pool
import time
# 計算平方數
def square(n):
print('calculate square of %d' % n)
time.sleep(1)
return n * n
if __name__=='__main__':
# 創建進程池對象
with Pool(processes=3) as pool:
# 調用map方法執行任務
results=pool.map(square, [1, 2, 3, 4, 5])
print(results)
```
上面的代碼中,我們首先定義了一個函數`square`,用于計算一個數的平方數。在主進程中,我們使用`multiprocessing.Pool()`函數創建了一個進程池對象`pool`,并指定了進程數為3。然后,我們調用`pool.map()`方法提交任務,并使用`print()`打印出返回結果。
輸出結果如下:
```
calculate square of 1
calculate square of 2
calculate square of 3
calculate square of 4
calculate square of 5
[1, 4, 9, 16, 25]
```
可以看到,進程池成功地執行了我們提交的任務,并返回了預期的結果。
除了map方法外,進程池還提供了apply()、apply_async()、imap()等方法,具體用法可以參考官方文檔。
六、多進程編程實踐
在實際應用中,我們經常會遇到需要大量計算的任務,比如圖像處理、機器學習等。使用多進程可以極大地提高計算效率,加快任務完成速度。下面是一個簡單的例子,演示如何使用多進程進行圖像處理:
```python
from PIL import Image
import numpy as np
from multiprocessing import Pool
# 圖像縮放函數
def resize(img_path, size):
img=Image.open(img_path)
img=img.resize(size)
return img
if __name__=='__main__':
# 加載圖像列表
img_list=['image1.jpg', 'image2.jpg', 'image3.jpg', 'image4.jpg']
# 創建進程池對象
with Pool(processes=4) as pool:
# 調用map方法執行任務
results=pool.starmap(resize, [(img_path, (512, 512)) for img_path in img_list])
# 將結果保存到文件
for i, img in enumerate(results):
img.save('result%d.jpg' % (i+1))
```
上面的代碼中,我們首先定義了一個函數`resize`,用于對一張圖片進行縮放操作。然后,我們使用`PIL.Image.open()`函數加載了4張圖片,并使用進程池對它們進行縮放操作。最后,我們將結果保存到文件中。
由于進程池使用了4個進程同時進行圖像處理,因此可以大大提高圖像處理效率。
七、總結
本文介紹了Python中的多進程編程,并詳細講解了進程的基本概念、如何創建和啟動進程、進程間通信、進程池等內容。同時,我們還演示了一個簡單的實例,展示了如何使用多進程來加速圖像處理任務。
多進程編程是Python中非常重要的話題之一,掌握多進程編程對于開發高性能、高并發應用具有重要意義。希望本文能夠幫助讀者更好地理解并掌握Python多進程編程技巧。
一、異步編程概述
異步編程是一種并發處理的方式,它在單線程下實現多個任務同時執行的效果,從而提高程序的整體性能。在傳統的同步編程模型中,一個任務必須等待另一個任務完成后才能繼續執行,這種模型會造成大量的 CPU 時間浪費。而在異步編程模型中,任務可以像輪流使用 CPU 一樣交替執行,從而避免了 CPU 的空閑時間。
Python 的異步編程主要通過 asyncio 模塊來實現,它提供了對協程的支持,使得開發者可以用比較簡單的語法來編寫高效的異步程序。在 asyncio 中,協程是異步編程的基本單位,可以理解為一個特殊的函數。
二、協程概述
協程是一種能夠暫停執行并在需要時恢復執行的函數,它不同于線程和進程的異步編程方式。協程可以看作是一種用戶級線程,也稱為“微線程”,通常運行在單線程中,由協程調度程序切換執行。
Python 3.4 引入了 asyncio 庫,內置了協程功能,此后 Python 開始支持原生的協程編程。在 Python 3.5 中,新引入了 async 和 await 兩個關鍵字,更加方便了協程的編寫。具體來說,async 關鍵字用于聲明一個函數為協程函數,而 await 關鍵字用于暫停當前協程的執行,等待其他協程或者異步任務執行完成后再繼續執行。
在 asyncio 中,協程由 asyncio.coroutine 裝飾器進行修飾,這樣被修飾的函數就可以使用 await 關鍵字暫停當前協程的執行了。下面是一個簡單的協程示例:
```python
import asyncio
async def coroutine():
print("start coroutine")
await asyncio.sleep(1)
print("end coroutine")
# 初始化事件循環
loop=asyncio.get_event_loop()
# 執行協程
loop.run_until_complete(coroutine())
```
上面的代碼中,我們首先定義了一個名為 coroutine 的協程函數,它打印出一條開始執行的消息,并調用 asyncio.sleep() 函數讓協程掛起 1 秒鐘,最后打印出一條結束執行的消息。
在主程序中,我們通過 asyncio.get_event_loop() 函數獲取到一個事件循環對象,然后調用該對象的 run_until_complete() 方法來運行協程。
三、協程與線程的比較
協程和線程都可以實現異步編程,但二者有很大的不同之處。下面是協程與線程的比較:
1. 狀態切換方式不同:線程是由操作系統內核來進行調度的,而協程則是由程序員自己實現的調度方式。
2. 調度開銷不同:線程切換時需要保存當前上下文并在下一次切換時恢復,這個過程需要進行上下文切換和內核態和用戶態之間的轉換,因此比較耗費 CPU 時間。而協程的上下文切換僅僅是程序棧的切換,沒有內核態和用戶態之間的切換,因此非常快速。
3. 并發性不同:線程是操作系統級別的并發,可以利用多核 CPU 實現真正的并行處理;而協程則是單線程的并發,它不能充分利用多核 CPU 的優勢,但有著更高的并發性能。
4. 編寫難度不同:線程編程的難度較大,需要考慮線程安全和鎖的問題,而協程編程則相對容易一些,不需要考慮這些問題。
綜合來看,協程比線程更適用于 I/O 密集型應用程序,而線程則適用于 CPU 密集型應用程序。
四、asyncio 庫的使用
1. 事件循環
事件循環是 asyncio 的核心組件,它可以在單線程下運行多個協程,并負責調度它們的執行。每個 asyncio 程序必須有一個事件循環對象,我們可以通過 asyncio.get_event_loop() 函數獲取到它。
```python
import asyncio
# 獲取事件循環對象
loop=asyncio.get_event_loop()
# 執行協程
loop.run_until_complete(coroutine())
```
2. 協程函數
協程函數是由 async 關鍵字修飾的函數,它可以被 await 關鍵字掛起和恢復執行。下面是一個簡單的協程函數示例:
```python
import asyncio
async def coroutine():
print("start coroutine")
await asyncio.sleep(1)
print("end coroutine")
```
3. 異步任務
異步任務是一個可等待對象,它可以被事件循環掛起和恢復執行。在 asyncio 中,常見的異步任務包括協程、Future 對象、Task 對象等。下面是異步任務的一個簡單示例:
```python
import asyncio
async def coroutine():
print("start coroutine")
await asyncio.sleep(1)
print("end coroutine")
async def async_func():
print("start async function")
await coroutine()
print("end async function")
# 獲取事件循環對象
loop=asyncio.get_event_loop()
# 執行異步任務
loop.run_until_complete(async_func())
```
4. Future 對象
Future 是一個代表異步操作結果的對象,它可以被用于協程間通信和協調。如果一個協程需要等待另一個協程完成某個操作后才能繼續執行,那么它可以通過 Future 對象來實現。下面是一個使用 Future 對象進行協程間通信的示例:
```python
import asyncio
async def coroutine(future):
print("start coroutine")
await asyncio.sleep(1)
future.set_result("hello world")
print("end coroutine")
async def async_func():
print("start async function")
future=asyncio.Future()
asyncio.ensure_future(coroutine(future))
result=await future
print(result)
# 獲取事件循環對象
loop=asyncio.get_event_loop()
# 執行異步任務
loop.run_until_complete(async_func())
```
在上面的代碼中,我們首先定義了一個 coroutine 協程函數,并傳入了一個 future 對象。在該函數內部,我們使用 asyncio.sleep() 函數模擬一個耗時的操作,并使用 future.set_result() 方法設置異步操作的返回值。在 async_func 函數中,我們監聽 future 對象的完成狀態,并在異步操作完成后打印出返回值。
5. Task 對象
Task 對象是對 Future 對象的進一步封裝,它可以方便地對多個協程進行調度和管理。Task 對象通常是通過 asyncio.create_task() 函數創建的。下面是一個使用 Task 對象進行協程調度的示例:
```python
import asyncio
async def coroutine(name):
print("start %s" % name)
await asyncio.sleep(1)
print("end %s" % name)
async def async_func():
tasks=[asyncio.create_task(coroutine("coroutine%d" % i)) for i in range(3)]
for task in tasks:
await task
# 獲取事件循環對象
loop=asyncio.get_event_loop()
# 執行異步任務
loop.run_until_complete(async_func())
```
在上面的代碼中,我們先定義了一個 coroutine 協程函數,并傳入了一個名字參數。然后,在 async_func 函數內部,我們使用 asyncio.create_task() 函數創建了三個協程任務,并將它們添加到任務列表中。最后,我們使用 for 循環逐個執行每個任務。
6. 同步與異步的混合使用
在實際應用中,我們常常需要在異步程序中調用同步函數或者阻塞操作,這時候就需要使用 asyncio 的一些輔助函數來避免阻塞事件循環。下面是一些常用的 asyncio 輔助函數:
- asyncio.run_in_executor():在另一個線程或進程中執行函數,并將結果返回給當前協程。
```python
import asyncio
import requests
async def coroutine():
print("start coroutine")
loop=asyncio.get_running_loop()
future=loop.run_in_executor(None, requests.get, "https://www.baidu.com")
response=await future
print(response.status_code)
print("end coroutine")
# 獲取事件循環對象
loop=asyncio.get_event_loop()
# 執行協程
loop.run_until_complete(coroutine())
```
- asyncio.sleep():模擬一個耗時的操作,并掛起當前協程的執行。
```python
import asyncio
async def coroutine():
print("start coroutine")
await asyncio.sleep(1)
print("end coroutine")
# 獲取事件循環對象
loop=asyncio.get_event_loop()
# 執行協程
loop.run_until_complete(coroutine())
```
- asyncio.wait():等待多個協程任務完成后再繼續執行。
```python
import asyncio
async def coroutine(name):
print("start %s" % name)
await asyncio.sleep(1)
print("end %s" % name)
async def async_func():
tasks=[asyncio.create_task(coroutine("coroutine%d" % i)) for i in range(3)]
await asyncio.wait(tasks)
# 獲取事件循環對象
loop=asyncio.get_event_loop()
# 執行異步任務
loop.run_until_complete(async_func())
```
五、總結
本文介紹了 Python 異步編程的基本概念,包括協程、事件循環、異步任務、Future 對象和 Task 對象等。我們還比較了協程與線程的不同之處,并且講解了如何在異步程序中使用同步函數或阻塞操作。
異步編程可以大幅度提高程序的并發性能,特別適用于 I/O 密集型應用程序。Python 的 asyncio 模塊為開發者提供了非常方便的協程支持,使得編寫高效的異步程序變得簡單易行。同時,在實際應用中需謹慎使用異步編程,避免造成過度使用導致的代碼維護困難和可讀性差等問題。