欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

如何讓python程序正確高效地并發(fā)

 更新時間:2022年06月08日 16:40:56   作者:? bastgia?  ?  
這篇文章主要介紹了如何讓python程序正確高效地并發(fā),文章圍繞主題的相關(guān)資料展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下

前言:

如今,大多數(shù)計算機都帶有多個內(nèi)核,允許多個線程并行運行計算。即使處理器只有單核,也可以通過并發(fā)編程來提升程序的運行效率,比如在一個線程等待網(wǎng)絡(luò)數(shù)據(jù)的同時,允許另一個線程占用CPU完成計算操作。并發(fā)編程對于程序運行加速是非常重要的。

不幸的是,由于所謂的全局解釋器鎖(“GIL”),在許多情況下,Python 一次只能運行一個線程。只有在一些特定的場景下,它才可以很好地運行多個線程。

但是哪些使用模式允許并行,哪些不允許?因此,本文將以實用性的角度解析 GIL 的工作原理,逐步深化對于GIL的認知:

  • 本文將由淺入深的講解GIL的工作原理,并把GIL的特性由淺入深的抽象成認知模型從而方便理解
  • 本文將給出一些實用的設(shè)計方法,幫助讀者預(yù)測并行瓶頸是否出現(xiàn)以及出現(xiàn)的位置

太長不看版:

線程必須持有 GIL 才能調(diào)用 CPython C API。**

在解釋器中運行的 Python 代碼,例如 x = f(1, 2),會使用這些 API。 每個 == 比較、每個整數(shù)加法、每個 list.append:都需要調(diào)用 CPython C API。 因此,線程運行 Python 代碼時必須持有鎖。

其他線程無法獲取 GIL,因此無法運行,直到當(dāng)前運行的線程釋放它,這會自動每 5ms 發(fā)生一次。

長時間運行(“阻塞”)的擴展代碼會阻止自動切換。

然而,用 C(或其他低級語言)編寫的 Python 擴展可以顯式釋放 GIL,從而允許一個或多個線程與持有 GIL 的線程并行運行。

python線程何時需要擁有GIL?

GIL 是 CPython 解釋器的實現(xiàn)的一部分,它是一個線程鎖:在一個給定的時間只有一個線程可以獲取鎖。因此,要了解 GIL 如何影響 Python 的多線程并行能力,我們首先需要回答一個關(guān)鍵問題:Python 線程何時需要持有 GIL?

認知模型1:同一時刻只有一個線程運行python代碼

考慮以下代碼; 它在兩個線程中運行函數(shù) go():

import threading
import time
def go():
    start = time.time()
    while time.time() < start + 0.5:
        sum(range(10000))
def main():
    threading.Thread(target=go).start()
    time.sleep(0.1)
    go()
main()

當(dāng)我們使用 Sciagraph 性能分析器運行它時,執(zhí)行時間線如下所示:

注意:線程是如何在 CPU 上等待和運行之間來回切換的:運行代碼持有 GIL,等待線程正在等待 GIL。

如果 GIL 5 毫秒(或其他可配置的時間間隔)沒有釋放,Python 會告訴當(dāng)前正在運行的線程釋放 GIL。下一個線程拿到GIL后就可以運行。如上圖所示,我們看到兩個線程之間來回切換;實際顯示的間隔長于 5 毫秒,因為采樣分析器每 47 毫秒左右采樣一次。

這就是我們最初的認知模型,或者說是對于GIL最淺層的認知:

  • 線程必須持有 GIL 才能運行 Python 代碼。
  • 其他線程無法獲取 GIL,因此無法運行,直到當(dāng)前運行的線程釋放它,GIL的切換每 5ms 進行一次。

模型2:不保證每 5 毫秒釋放一次 GIL

GIL 在 Python 3.7 到 3.10 中默認每 5ms 釋放一次,從而允許其他線程運行:

>>> import sys
>>> sys.getswitchinterval()
0.005

但是,這些版本中的GIL是盡力而為的,也就是說,其不能保證每隔5ms一定使得線程釋放??紤]一個簡單的偽代碼,解釋器在運行python線程時的邏輯如這個偽代碼中的死循環(huán)所示:只有運行完一個操作后解釋器python才會去檢查是否釋放GIL鎖。

當(dāng)然,python內(nèi)部的實現(xiàn)邏輯比這個偽代碼復(fù)雜的多,但是遵循的原則是相同的:

while True:
    if time_to_release_gil():
        temporarily_release_gil()
    run_next_python_instruction()

只要 run_next_python_instruction() 沒有完成,temporary_release_gil() 就不會被調(diào)用。 大多數(shù)情況下,這不會發(fā)生,因為單個操作(添加兩個整數(shù)、追加到列表等)很快就可以完成。因此,解釋器可以經(jīng)常檢查是否該釋放GIL。

但是,長時間運行的操作會阻止 GIL 自動釋放。 讓我們編寫一個小的Cython拓展,Cython是一種類似 Python的語言,其代碼會轉(zhuǎn)化成C/C++代碼,并編譯成可以被python調(diào)用的形式。下邊的代碼調(diào)用標準 C 庫中的 sleep() 函數(shù):

cdef extern from "unistd.h":
    unsigned int sleep(unsigned int seconds)
def c_sleep(unsigned int seconds):
    sleep(seconds)

我們可以使用 Cython 附帶的 cythonize 工具將其編譯為可導(dǎo)入的 Python 擴展:

$ cythonize -i c_sleep.pyx
...
$ ls c_sleep*.so
c_sleep.cpython-39-x86_64-linux-gnu.so

接下來從一個 Python 程序中調(diào)用它,該程序會創(chuàng)建一個新線程,并調(diào)用c_sleep(),該新線程與主線程是并行的:

import threading
import time
from c_sleep import c_sleep

def thread():
    c_sleep(2)
threading.Thread(target=thread).start()
start = time.time()
while time.time() < start + 2:
    sum(range(10000))

直到睡眠線程完成前,主線程無法運行;睡眠線程根本沒有釋放 GIL。這是因為python在調(diào)用底層語言(如C)所編寫的模塊時是阻塞性的調(diào)用,只有等到調(diào)用返回結(jié)果之后,本條語句才算執(zhí)行結(jié)束。而對 c_sleep(2) 的調(diào)用在2秒內(nèi)沒有返回。在這2秒結(jié)束之前,Python 解釋器循環(huán)不會運行,因此不會檢查它是否應(yīng)該自動釋放 GIL。

這是我們深化后的對GIL的認知:

  • Python 線程必須持有 GIL 才能運行代碼。
  • 其他 Python 線程無法獲取 GIL,因此無法運行,直到當(dāng)前運行的線程釋放它,這會自動每 5 毫秒發(fā)生一次。
  • 長時間運行(“阻塞”)的擴展代碼會阻止自動切換。

模型3:非 Python 代碼可以顯式釋放 GIL

time.sleep(3)使得線程3秒內(nèi)什么都不做。如上所述,運行時間較長的拓展代碼會阻止GIL在線程之間的自動切換。那么這是否意味當(dāng)某一線程運行time.sleep()時,其他線程也不能運行?

讓我們試試下面的代碼,它嘗試在主線程中并行運行 3 秒的睡眠和 5 秒的計算:

import threading
from time import time, sleep

program_start = time()

def thread():
    sleep(3)
    print("Sleep thread done, elapsed:", time() - program_start)

threading.Thread(target=thread).start()

# 在主線程中進行5秒的計算:
calc_start = time()
while time() < calc_start + 5:
    sum(range(10000))
print("Main thread done, elapsed:", time() - program_start)

運行后的結(jié)果為:

$ time python gil2.py 
Sleep thread done, elapsed: 3.0081260204315186
Main thread done, elapsed: 5.000330924987793
real    0m5.068s
user    0m4.977s
sys     0m0.011s

如果程序只能單線程的運行,那么程序運行時長需要8秒,3秒用于睡眠,5秒用于計算。從上邊的結(jié)果可以看出,睡眠線程和主線程并行運行!

Sciagraph 性能分析器的輸出如下圖所示:

想要了解這個現(xiàn)象的原因,需要我們閱讀time.sleep的實現(xiàn)代碼:

        int ret;
        Py_BEGIN_ALLOW_THREADS
#ifdef HAVE_CLOCK_NANOSLEEP
        ret = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &timeout_abs, NULL);
        err = ret;
#elif defined(HAVE_NANOSLEEP)
        ret = nanosleep(&timeout_ts, NULL);
        err = errno;
#else
        ret = select(0, (fd_set *)0, (fd_set *)0, (fd_set *)0, &timeout_tv);
        err = errno;
#endif
        Py_END_ALLOW_THREADS

根據(jù) PY_BEGIN/END_ALLOW_THREADS 的文檔,Py_BEGIN_ALLOW_THREADS會使得程序自動的釋放GIL鎖,然后去執(zhí)行阻塞操作,當(dāng)程序運行到Py_END_ALLOW_THREADS時才會申請GIL鎖。因此,上邊的C實現(xiàn)在調(diào)用底層操作系統(tǒng)睡眠函數(shù)時會顯式釋放GIL。這是GIL釋放的另一種方式,它與我們目前知道的每 5 毫秒自動切換一次是相互獨立的。

任何已釋放 GIL 并且不嘗試申請它的代碼(比如上文的sleep()期間)都不會阻塞其他申請GIL的線程。 因此,只要程序能夠顯式釋放 GIL,我們可以并行運行任意數(shù)量的線程。

所以這是我們的第三層認知:

  • 線程必須持有 GIL 才能運行 Python 代碼。
  • 其他線程無法獲取 GIL,因此無法運行,直到當(dāng)前運行的線程釋放它,這會自動每 5ms 發(fā)生一次。
  • 長時間運行(“阻塞”)的擴展代碼會阻止自動切換。
  • 然而,用 C(或其他低級語言)編寫的 Python 擴展可以顯式釋放 GIL,從而允許一個或多個線程與持有 GIL 的線程并行運行。

模型4:調(diào)用 Python C API 需要 GIL

到目前為止,我們已經(jīng)說過python調(diào)用的C代碼能夠在某些情況下主動釋放GIL。但是,線程調(diào)用 CPython C API時都必須持有 GIL。

當(dāng)線程調(diào)用CPython C API時必須持有GIL,只有很少的API不需要持有GIL

(CPython C API可以使得Python程序調(diào)用已編譯的利用C/C++編寫的代碼片段,Python 語言和標準庫的大部分核心功能都是用 C 編寫的)

所以這是我們最終的認知模型:

  • 線程必須持有 GIL 才能調(diào)用 CPython C API。
  • 在解釋器中運行的 Python 代碼,例如 x = f(1, 2),會使用這些 API。 每個 == 比較、每個整數(shù)加法、每個 list.append:都需要調(diào)用 CPython C API。 因此,線程運行 Python 代碼時必須持有鎖。
  • 其他線程無法獲取 GIL,因此無法運行,直到當(dāng)前運行的線程釋放它,這會自動每 5ms 發(fā)生一次。
  • 長時間運行(“阻塞”)的擴展代碼會阻止自動切換。
  • 然而,用 C(或其他低級語言)編寫的 Python 擴展可以顯式釋放 GIL,從而允許一個或多個線程與持有 GIL 的線程并行運行。

什么場景適合利用python的并發(fā)?

當(dāng)調(diào)用運行時間較長的,用C編寫的API時應(yīng)當(dāng)主動釋放GIL

python多線程最有用的情況是,線程調(diào)用長時間運行的C/C++/RUST代碼,因此會長時間的不需要調(diào)用CPython C API,此時就可以讓線程釋放GIL從而允許其他線程運行。

不適合并發(fā)的場景:

所謂的純python代碼,指的是代碼只與python內(nèi)置的對象,如字典,整數(shù),列表交互,并且代碼也不會阻塞性的調(diào)用底層代碼,這樣的代碼會頻繁地使用Python C API:

l = []
for i in range(i):
    l.append(i * i)

此時搞線程并發(fā)并沒有太大的意義

使用Python C API的低級代碼

另一種不會獲得太多并行性的情況是:在C/Rust擴展中需要使用大量的Python C API。例如,考慮一個讀取以下字符串的 JSON 解析器:

[1, 2, 3]

解析器將:

  • 讀取幾個字節(jié),然后創(chuàng)建一個 Python 列表。
  • 然后它將讀取更多字節(jié),然后創(chuàng)建一個 Python 整數(shù)并將其附加到列表中。
  • 這種情況一直持續(xù)到數(shù)據(jù)處理完為止。

創(chuàng)建所有這些 Python 對象需要使用 CPython C API,因此需要持有 GIL。由于反復(fù)占有和釋放 GIL 會降低程序的性能,而且大多數(shù) JSON 文檔都可以非??焖俚亟馕?。 因此,JSON解析器的開發(fā)者當(dāng)然會選擇在整個處理過程結(jié)束之前都不釋放GIL,但這也導(dǎo)致json解析器解析期間,程序只能線性運行。

讓我們通過觀察當(dāng)我們在兩個線程中讀取兩個大文檔時,Python的內(nèi)置JSON解析器如何影響并行性來驗證這個假設(shè)。代碼如下所示:

import json
import threading

def load_json():
    with open("large.json") as f:
        return json.load(f)

threading.Thread(target=load_json).start()
load_json()

性能分析器的結(jié)果如下所示:

很明顯,同時運行兩個json解析器時,線程之間完全沒有并行

到此這篇關(guān)于如何讓python程序正確高效地并發(fā)的文章就介紹到這了,更多相關(guān) python程 高效并發(fā)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Pytorch中torch.unsqueeze()與torch.squeeze()函數(shù)詳細解析

    Pytorch中torch.unsqueeze()與torch.squeeze()函數(shù)詳細解析

    torch.squeeze()這個函數(shù)主要對數(shù)據(jù)的維度進行壓縮,去掉維數(shù)為1的的維度,下面這篇文章主要給大家介紹了關(guān)于Pytorch中torch.unsqueeze()與torch.squeeze()函數(shù)詳細的相關(guān)資料,需要的朋友可以參考下
    2023-02-02
  • python re模塊的高級用法詳解

    python re模塊的高級用法詳解

    這篇文章較詳細的給大家介紹了python re模塊的高級用法,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友參考下吧
    2018-06-06
  • Python基于均值漂移算法和分水嶺算法實現(xiàn)圖像分割

    Python基于均值漂移算法和分水嶺算法實現(xiàn)圖像分割

    圖像分割是將圖像分成若干具有獨特性質(zhì)的區(qū)域并提取感興趣目標的技術(shù)和過程。這篇文章將詳細講解基于均值漂移算法和分水嶺算法的圖像分割,需要的可以參考一下
    2023-01-01
  • python輸出pdf文檔的實例

    python輸出pdf文檔的實例

    今天小編就為大家分享一篇python輸出pdf文檔的實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-02-02
  • 如何將yolo格式轉(zhuǎn)化為voc格式:txt轉(zhuǎn)xml(親測有效)

    如何將yolo格式轉(zhuǎn)化為voc格式:txt轉(zhuǎn)xml(親測有效)

    這篇文章主要介紹了如何將yolo格式轉(zhuǎn)化為voc格式:txt轉(zhuǎn)xml,親測有效,可以使用,本文通過圖文并茂的形式給大家介紹的非常詳細,感興趣的朋友參考下吧
    2023-12-12
  • Numpy數(shù)組的切片索引操作

    Numpy數(shù)組的切片索引操作

    本文主要介紹了Numpy數(shù)組的切片索引操作,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-06-06
  • 詳解Python連接MySQL數(shù)據(jù)庫的多種方式

    詳解Python連接MySQL數(shù)據(jù)庫的多種方式

    這篇文章主要介紹了Python連接MySQL數(shù)據(jù)庫方式,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • 簡單了解django orm中介模型

    簡單了解django orm中介模型

    這篇文章主要介紹了簡單了解django orm中介模型,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-07-07
  • python中reversed與reverse的區(qū)別解析

    python中reversed與reverse的區(qū)別解析

    reverse()是python中列表的一個內(nèi)置方法(在字典、字符串和元組中沒有這個內(nèi)置方法),用于列表中數(shù)據(jù)的反轉(zhuǎn),這篇文章主要介紹了python中reversed與reverse的區(qū)別,需要的朋友可以參考下
    2023-03-03
  • windows環(huán)境中利用celery實現(xiàn)簡單任務(wù)隊列過程解析

    windows環(huán)境中利用celery實現(xiàn)簡單任務(wù)隊列過程解析

    這篇文章主要介紹了windows環(huán)境中利用celery實現(xiàn)簡單任務(wù)隊列過程解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-11-11

最新評論