一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

深入理解 GIL:如何寫出高性能及線程安全的 Python 代碼

 DerekW99 2017-05-26


編譯:伯樂在線 - 鄭蕓

如有好文章投稿,請點擊 → 這里了解詳情


6歲時,我有一個音樂盒。我上緊發(fā)條,音樂盒頂上的芭蕾舞女演員就會旋轉(zhuǎn)起來,同時,內(nèi)部裝置發(fā)出“一閃一閃亮晶晶,滿天都是小星星”的叮鈴聲。那玩意兒肯定俗氣透了,但我喜歡那個音樂盒,我想知道它的工作原理是什么。后來我拆開了,才看到它里面一個簡單的裝置,機身內(nèi)部鑲嵌著一個拇指大小的金屬圓筒,當它轉(zhuǎn)動時會撥弄鋼制的梳齒,從而發(fā)出這些音符。



在一個程序員具備的所有特性中,想探究事物運轉(zhuǎn)規(guī)律的這種好奇心必不可少。當我打開音樂盒,觀察內(nèi)部裝置,可以看出即使我沒有成長為一個卓越的程序員,至少也是有好奇心的一個。


奇怪的是,我寫 Python 程序多年,一直對全局解釋器鎖(GIL)持有錯誤的觀念,因為我從未對它的運作機理產(chǎn)生足夠好奇。我遇到其他對此同樣猶豫和無知的人。是時候讓我們來打開這個盒子一窺究竟了。讓我們解讀 CPython 解釋器源碼,找出 GIL 究竟是什么,為什么它存在于 Python 中,它又是怎么影響多線程程序的。我將通過舉例幫助你深入理解 GIL 。你將會學到如何寫出快速運行和線程安全的 Python 代碼,以及如何在線程和進程中做選擇。


(我在本文中只描述 CPython,而不是 Jython、PyPy 或  IronPython。因為目前絕大多數(shù)程序員還是使用 CPython 實現(xiàn) Python 。)


瞧,全局解釋器鎖(GIL)


這里:


static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */


這一行代碼摘自 ceval.c —— CPython 2.7 解釋器的源代碼,Guido van Rossum 的注釋”This is the GIL“ 添加于2003 年,但這個鎖本身可以追溯到1997年他的第一個多線程 Python 解釋器。在 Unix系統(tǒng)中,PyThread_type_lock 是標準 C  mutex_t 鎖的別名。當 Python 解釋器啟動時它初始化:


void

PyEval_InitThreads(void)

{

    interpreter_lock = PyThread_allocate_lock();

    PyThread_acquire_lock(interpreter_lock);

}


解釋器中的所有 C 代碼在執(zhí)行 Python 時必須保持這個鎖。Guido 最初加這個鎖是因為它使用起來簡單。而且每次從 CPython 中去除 GIL 的嘗試會耗費單線程程序太多性能,盡管去除 GIL 會帶來多線程程序性能的提升,但仍是不值得的。(前者是Guido最為關(guān)切的, 也是不去除 GIL 最重要的原因, 一個簡單的嘗試是在1999年, 最終的結(jié)果是導致單線程的程序速度下降了幾乎2倍.)


GIL 對程序中線程的影響足夠簡單,你可以在手背上寫下這個原則:“一個線程運行 Python ,而其他 N 個睡眠或者等待 I/O.”(即保證同一時刻只有一個線程對共享資源進行存取)  Python 線程也可以等待threading.Lock或者線程模塊中的其他同步對象;線程處于這種狀態(tài)也稱之為”睡眠“。



線程何時切換?一個線程無論何時開始睡眠或等待網(wǎng)絡(luò) I/O,其他線程總有機會獲取 GIL 執(zhí)行 Python 代碼。這是協(xié)同式多任務(wù)處理。CPython 也還有搶占式多任務(wù)處理。如果一個線程不間斷地在 Python 2 中運行 1000 字節(jié)碼指令,或者不間斷地在 Python 3 運行15 毫秒,那么它便會放棄 GIL,而其他線程可以運行。把這想象成舊日有多個線程但只有一個 CPU 時的時間片。我將具體討論這兩種多任務(wù)處理。


把 Python 看作是舊時的大型主機,多個任務(wù)共用一個CPU。


協(xié)同式多任務(wù)處理


當一項任務(wù)比如網(wǎng)絡(luò) I/O啟動,而在長的或不確定的時間,沒有運行任何 Python 代碼的需要,一個線程便會讓出GIL,從而其他線程可以獲取 GIL 而運行 Python。這種禮貌行為稱為協(xié)同式多任務(wù)處理,它允許并發(fā);多個線程同時等待不同事件。


也就是說兩個線程各自分別連接一個套接字:


def do_connect():

    s = socket.socket()

    s.connect(('python.org', 80))  # drop the GIL

 

for i in range(2):

    t = threading.Thread(target=do_connect)

    t.start()


兩個線程在同一時刻只能有一個執(zhí)行 Python ,但一旦線程開始連接,它就會放棄 GIL ,這樣其他線程就可以運行。這意味著兩個線程可以并發(fā)等待套接字連接,這是一件好事。在同樣的時間內(nèi)它們可以做更多的工作。


讓我們打開盒子,看看一個線程在連接建立時實際是如何放棄 GIL 的,在 socketmodule.c 中:


/* s.connect((host, port)) method */

static PyObject *

sock_connect(PySocketSockObject *s, PyObject *addro)

{

    sock_addr_t addrbuf;

    int addrlen;

    int res;

 

    /* convert (host, port) tuple to C address */

    getsockaddrarg(s, addro, SAS2SA(&addrbuf), &addrlen);

 

    Py_BEGIN_ALLOW_THREADS

    res = connect(s->sock_fd, addr, addrlen);

    Py_END_ALLOW_THREADS

 

    /* error handling and so on .... */

}


線程正是在Py_BEGIN_ALLOW_THREADS 宏處放棄 GIL;它被簡單定義為:


PyThread_release_lock(interpreter_lock);


當然 Py_END_ALLOW_THREADS 重新獲取鎖。一個線程可能會在這個位置堵塞,等待另一個線程釋放鎖;一旦這種情況發(fā)生,等待的線程會搶奪回鎖,并恢復(fù)執(zhí)行你的Python代碼。簡而言之:當N個線程在網(wǎng)絡(luò) I/O 堵塞,或等待重新獲取GIL,而一個線程運行Python。


下面來看一個使用協(xié)同式多任務(wù)處理快速抓取許多 URL 的完整例子。但在此之前,先對比下協(xié)同式多任務(wù)處理和其他形式的多任務(wù)處理。


搶占式多任務(wù)處理


Python線程可以主動釋放 GIL,也可以先發(fā)制人抓取 GIL 。


讓我們回顧下 Python 是如何運行的。你的程序分兩個階段運行。首先,Python文本被編譯成一個名為字節(jié)碼的簡單二進制格式。第二,Python解釋器的主回路,一個名叫 pyeval_evalframeex() 的函數(shù),流暢地讀取字節(jié)碼,逐個執(zhí)行其中的指令。


當解釋器通過字節(jié)碼時,它會定期放棄GIL,而不需要經(jīng)過正在執(zhí)行代碼的線程允許,這樣其他線程便能運行:


for (;;) {

    if (--ticker < 0) {

        ticker = check_interval;

 

        /* Give another thread a chance */

        PyThread_release_lock(interpreter_lock);

 

        /* Other threads may run now */

 

        PyThread_acquire_lock(interpreter_lock, 1);

    }

 

    bytecode = *next_instr ;

    switch (bytecode) {

        /* execute the next instruction ... */

    }

}


默認情況下,檢測間隔是1000 字節(jié)碼。所有線程都運行相同的代碼,并以相同的方式定期從他們的鎖中抽出。在 Python 3 GIL 的實施更加復(fù)雜,檢測間隔不是一個固定數(shù)目的字節(jié)碼,而是15 毫秒。然而,對于你的代碼,這些差異并不顯著。


Python中的線程安全


將多個線狀物編織在一起,需要技能。


如果一個線程可以隨時失去 GIL,你必須使讓代碼線程安全。 然而 Python 程序員對線程安全的看法大不同于 C 或者 Java 程序員,因為許多 Python 操作是原子的。


在列表中調(diào)用 sort(),就是原子操作的例子。線程不能在排序期間被打斷,其他線程從來看不到列表排序的部分,也不會在列表排序之前看到過期的數(shù)據(jù)。原子操作簡化了我們的生活,但也有意外。例如, = 似乎比 sort() 函數(shù)簡單,但 =不是原子操作。你怎么知道哪些操作是原子的,哪些不是?


看看這個代碼:


n = 0

def foo():

    global n

    n = 1


我們可以看到這個函數(shù)用 Python 的標準 dis 模塊編譯的字節(jié)碼:


>>> import dis

>>> dis.dis(foo)

LOAD_GLOBAL              0 (n)

LOAD_CONST               1 (1)

INPLACE_ADD

STORE_GLOBAL             0 (n)


代碼的一行中, n = 1,被編譯成 4 個字節(jié)碼,進行 4 個基本操作:


  1. 將 n 值加載到堆棧上

  2. 將常數(shù) 1 加載到堆棧上

  3. 將堆棧頂部的兩個值相加

  4. 將總和存儲回 n


記住,一個線程每運行 1000 字節(jié)碼,就會被解釋器打斷奪走 GIL 。如果運氣不好,這(打斷)可能發(fā)生在線程加載 n 值到堆棧期間,以及把它存儲回 n 期間。很容易可以看到這個過程會如何導致更新丟失:


threads = []

for i in range(100):

    t = threading.Thread(target=foo)

    threads.append(t)

for t in threads:

    t.start()

for t in threads:

    t.join()

print(n)


通常這個代碼輸出 100,因為 100 個線程每個都遞增 n 。但有時你會看到 99 或 98 ,如果一個線程的更新被另一個覆蓋。


所以,盡管有 GIL,你仍然需要加鎖來保護共享的可變狀態(tài):


n = 0

lock = threading.Lock()

def foo():

    global n

    with lock:

        n = 1


如果我們使用一個原子操作比如 sort() 函數(shù)會如何呢?:


lst = [4, 1, 3, 2]

def foo():

    lst.sort()


這個函數(shù)的字節(jié)碼顯示 sort() 函數(shù)不能被中斷,因為它是原子的:


>>> dis.dis(foo)

LOAD_GLOBAL              0 (lst)

LOAD_ATTR                1 (sort)

CALL_FUNCTION            0


一行被編譯成 3 個字節(jié)碼:


  1. 將 lst 值加載到堆棧上

  2. 將其排序方法加載到堆棧上

  3. 調(diào)用排序方法


即使這一行  lst.sort() 分幾個步驟,調(diào)用 sort 自身是單個字節(jié)碼,因此線程沒有機會在調(diào)用期間抓取 GIL 。我們可以總結(jié)為在 sort() 不需要加鎖。或者,為了避免擔心哪個操作是原子的,遵循一個簡單的原則:始終圍繞共享可變狀態(tài)的讀取和寫入加鎖。畢竟,在 Python 中獲取一個 threading.Lock 是廉價的。


盡管 GIL 不能免除我們加鎖的需要,但它確實意味著沒有加細粒度的鎖的需要(所謂細粒度是指程序員需要自行加、解鎖來保證線程安全,典型代表是 Java , 而 CPthon 中是粗粒度的鎖,即語言層面本身維護著一個全局的鎖機制,用來保證線程安全)。在線程自由的語言比如 Java,程序員努力在盡可能短的時間內(nèi)加鎖存取共享數(shù)據(jù),減輕線程爭奪,實現(xiàn)最大并行。然而因為在 Python 中線程無法并行運行,細粒度鎖沒有任何優(yōu)勢。只要沒有線程保持這個鎖,比如在睡眠,等待I/O, 或者一些其他失去 GIL 操作,你應(yīng)該使用盡可能粗粒度的,簡單的鎖。其他線程無論如何無法并行運行。


并發(fā)可以完成更快


我敢打賭你真正為的是通過多線程來優(yōu)化你的程序。通過同時等待許多網(wǎng)絡(luò)操作,你的任務(wù)將更快完成,那么多線程會起到幫助,即使在同一時間只有一個線程可以執(zhí)行 Python 。這就是并發(fā),線程在這種情況下工作良好。


線程中代碼運行更快


import threading

import requests

urls = [...]

def worker():

    while True:

        try:

            url = urls.pop()

        except IndexError:

            break  # Done.

        requests.get(url)

for _ in range(10):

    t = threading.Thread(target=worker)

    t.start()


正如我們所看到的,在 HTTP上面獲取一個URL中,這些線程在等待每個套接字操作時放棄 GIL,所以他們比一個線程更快完成工作。


Parallelism 并行


如果想只通過同時運行 Python 代碼,而使任務(wù)完成更快怎么辦?這種方式稱為并行,這種情況 GIL 是禁止的。你必須使用多個進程,這種情況比線程更復(fù)雜,需要更多的內(nèi)存,但它可以更好利用多個 CPU。


這個例子 fork 出 10 個進程,比只有 1 個進程要完成更快,因為進程在多核中并行運行。但是 10 個線程與 1 個線程相比,并不會完成更快,因為在一個時間點只有 1 個線程可以執(zhí)行 Python:


import os

import sys

nums =[1 for _ in range(1000000)]

chunk_size = len(nums) // 10

readers = []

while nums:

    chunk, nums = nums[:chunk_size], nums[chunk_size:]

    reader, writer = os.pipe()

    if os.fork():

        readers.append(reader)  # Parent.

    else:

        subtotal = 0

        for i in chunk: # Intentionally slow code.

            subtotal = i

        print('subtotal %d' % subtotal)

        os.write(writer, str(subtotal).encode())

        sys.exit(0)

# Parent.

total = 0

for reader in readers:

    subtotal = int(os.read(reader, 1000).decode())

    total = subtotal

print('Total: %d' % total)


因為每個 fork 的進程有一個單獨的 GIL,這個程序可以把工作分派出去,并一次運行多個計算。


(Jython 和 IronPython 提供單進程的并行,但它們遠沒有充分實現(xiàn) CPython 的兼容性。有軟件事務(wù)內(nèi)存的 PyPy 有朝一日可以運行更快。如果你對此好奇,試試這些解釋器。)


結(jié)語


既然你已經(jīng)打開了音樂盒,看到了它簡單的裝置,你明白所有你需要知道的如何寫出快速運行,線程安全的 Python 代碼。使用線程進行并發(fā) I/O 操作,在進程中進行并行計算。這個原則足夠簡單,你甚至不需要把它寫在你的手上。


看完本文有收獲?請轉(zhuǎn)發(fā)分享給更多人

關(guān)注「Python開發(fā)者」,提升Python技能

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    国产亚洲成av人在线观看| 国产成人亚洲综合色就色| 加勒比日本欧美在线观看| 中文字幕中文字幕一区二区| 欧美日韩校园春色激情偷拍| 日韩欧美国产精品自拍| 日韩人妻精品免费一区二区三区 | 久一视频这里只有精品| 空之色水之色在线播放| 女人高潮被爽到呻吟在线观看| 久久99这里只精品热在线| 日本特黄特色大片免费观看| 欧美日韩视频中文字幕| 在线日韩中文字幕一区| 91日韩欧美国产视频| 日韩精品视频免费观看| 99在线视频精品免费播放| 东京热一二三区在线免| 亚洲专区中文字幕在线| 亚洲一区二区精品免费视频| 欧美熟妇喷浆一区二区| 亚洲精品深夜福利视频| 成人精品日韩专区在线观看| 欧美成人黄色一区二区三区| 亚洲美女国产精品久久| 国产色一区二区三区精品视频| 成人你懂的在线免费视频| 欧美日韩国产免费看黄片| 欧美精品在线观看国产| 精品一区二区三区中文字幕| 一二区中文字幕在线观看| 欧美性高清一区二区三区视频 | 亚洲二区欧美一区二区| 国产精品一区二区三区日韩av| 91人妻人人精品人人爽| 亚洲中文字幕熟女丝袜久久| 日本加勒比在线播放一区| 成人精品日韩专区在线观看| 99国产高清不卡视频| 精品久久综合日本欧美| 一区二区三区国产日韩|