国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

如何編寫快速且線程安全的Python代碼

B0B0 / 1190人閱讀

摘要:其次,解釋器的主循環(huán),一個(gè)名為的函數(shù),讀取字節(jié)碼并逐個(gè)執(zhí)行其中的指令。所有線程都運(yùn)行相同的代碼,并以相同的方式定期從它們獲取鎖定。無(wú)論如何,其他線程無(wú)法并行運(yùn)行。

概述

如今我也是使用Python寫代碼好多年了,但是我卻很少關(guān)心GIL的內(nèi)部機(jī)制,導(dǎo)致在寫Python多線程程序的時(shí)候。今天我們就來(lái)看看CPython的源代碼,探索一下GIL的源碼,了解為什么Python里要存在這個(gè)GIL,過(guò)程中我會(huì)給出一些示例來(lái)幫助大家更好的理解GIL。

GIL概覽

有如下代碼:

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

這行代碼位于Python2.7源碼ceval.c文件里。在類Unix操作系統(tǒng)中,PyThread_type_lock對(duì)應(yīng)C語(yǔ)言里的mutex_t類型。在Python解釋器開(kāi)始運(yùn)行時(shí)初始化這個(gè)變量

void
PyEval_InitThreads(void)
{
    interpreter_lock = PyThread_allocate_lock();
    PyThread_acquire_lock(interpreter_lock);
}

所有Python解釋器里執(zhí)行的c代碼都必須獲取這個(gè)鎖,作者一開(kāi)始為求簡(jiǎn)單,所以使用這種單線程的方式,后來(lái)每次想移除時(shí),都發(fā)現(xiàn)代價(jià)太高了。

GIL對(duì)程序中的線程的影響很簡(jiǎn)單,你可以在手背上寫下這個(gè)原則:“一個(gè)線程運(yùn)行Python,而另外一個(gè)線程正在等待I / O.”Python代碼可以使用threading.Lock或者其他同步對(duì)象,來(lái)釋放CPU占用,讓其他程序得以執(zhí)行。

什么時(shí)候線程切換? 每當(dāng)線程開(kāi)始休眠或等待網(wǎng)絡(luò)I / O時(shí),另一個(gè)線程都有機(jī)會(huì)獲取GIL并執(zhí)行Python代碼。CPython還具有搶先式多任務(wù)處理:如果一個(gè)線程在Python 2中不間斷地運(yùn)行1000個(gè)字節(jié)碼指令,或者在Python 3中運(yùn)行15毫秒,那么它就會(huì)放棄GIL而另一個(gè)線程可能會(huì)運(yùn)行。

協(xié)作式多任務(wù)

每當(dāng)運(yùn)行一個(gè)任務(wù),比如網(wǎng)絡(luò)I/O,持續(xù)的時(shí)間很長(zhǎng)或者無(wú)法確定運(yùn)行時(shí)間,這時(shí)可以放棄GIL,這樣另一個(gè)線程就可以接受并運(yùn)行Python。 這種行為稱為協(xié)同多任務(wù),它允許并發(fā); 許多線程可以同時(shí)等待不同的事件。
假設(shè)有兩個(gè)鏈接socket的線程

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()

這兩個(gè)線程中一次只有一個(gè)可以執(zhí)行Python,但是一旦線程開(kāi)始連接,它就會(huì)丟棄GIL,以便其他線程可以運(yùn)行。這意味著兩個(gè)線程都可以等待它們的套接字同時(shí)連接,他們可以在相同的時(shí)間內(nèi)完成更多的工作。
接下來(lái),讓我們打開(kāi)Python的源碼,來(lái)看看內(nèi)部是如何實(shí)現(xiàn)的(位于socketmodule.c文件里):

 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,他的定義很簡(jiǎn)單:

PyThread_release_lock(interpreter_lock);

Py_END_ALLOW_THREADS用于獲取GIL鎖,這時(shí),當(dāng)前現(xiàn)在有可能會(huì)卡住,等待其他現(xiàn)在釋放GIL鎖。

優(yōu)先權(quán)式多任務(wù)

Python線程可以自愿釋放GIL,但它也可以搶先獲取GIL。
讓我們回顧一下如何執(zhí)行Python。 您的程序分兩個(gè)階段運(yùn)行。 首先,您的Python文本被編譯為更簡(jiǎn)單的二進(jìn)制格式,稱為字節(jié)碼。 其次,Python解釋器的主循環(huán),一個(gè)名為PyEval_EvalFrameEx()的函數(shù),讀取字節(jié)碼并逐個(gè)執(zhí)行其中的指令。當(dāng)解釋器逐步執(zhí)行您的字節(jié)碼時(shí),它會(huì)定期刪除GIL,而不會(huì)詢問(wèn)正在執(zhí)行其代碼的線程的權(quán)限,因此其他線程可以運(yùn)行:

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 ... */ 
    }
}

默認(rèn)情況下,檢查間隔為1000個(gè)字節(jié)碼。 所有線程都運(yùn)行相同的代碼,并以相同的方式定期從它們獲取鎖定。 在Python 3中,GIL的實(shí)現(xiàn)更復(fù)雜,檢查間隔不是固定數(shù)量的字節(jié)碼,而是15毫秒。 但是,對(duì)于您的代碼,這些差異并不重要。

Python線程安全

如果某個(gè)線程在任何時(shí)候都可能丟失GIL,那么您必須使代碼具有線程安全性。 然而,Python程序員對(duì)線程安全的看法與C或Java程序員的不同,因?yàn)樵S多Python操作都是原子的。

原子操作的一個(gè)示例是在列表上調(diào)用sort()。 線程不能在排序過(guò)程中被中斷,其他線程永遠(yuǎn)不會(huì)看到部分排序的列表,也不會(huì)在列表排序之前看到過(guò)時(shí)的數(shù)據(jù)。 原子操作簡(jiǎn)化了我們的生活,但也有驚喜。 例如,+ =似乎比sort()簡(jiǎn)單,但+ =不是原子的。 那我們?cè)趺粗滥男┎僮魇窃拥模男┎皇牵?/p>

例如有代碼如下:

n = 0

def foo():
    global n
    n += 1

我們可以使用python的dis模塊獲取這段代碼對(duì)應(yīng)的字節(jié)碼:

>>> import dis
>>> dis.dis(foo)
LOAD_GLOBAL              0 (n)
LOAD_CONST               1 (1)
INPLACE_ADD
STORE_GLOBAL             0 (n)

可以看出,n += 1這行代碼,編譯出了4個(gè)字節(jié)碼:

將n的值加載到堆棧上

將常量1加載到堆棧上

將堆棧頂部的兩個(gè)值相加

將總和存回n

請(qǐng)記住,一個(gè)線程的每1000個(gè)字節(jié)碼被解釋器中斷以釋放GIL。 如果線程不幸運(yùn),這可能發(fā)生在它將n的值加載到堆棧上以及何時(shí)將其存儲(chǔ)回來(lái)之間。這樣就容易導(dǎo)致數(shù)據(jù)丟失:

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,因?yàn)?00個(gè)線程中的每一個(gè)都增加了1。 但有時(shí)你會(huì)看到99或98,這就是其中一個(gè)線程的更新被另一個(gè)線程覆蓋。所以,盡管有GIL,你仍然需要鎖來(lái)保護(hù)共享的可變狀態(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()

翻譯成字節(jié)碼如下:

>>> dis.dis(foo)
LOAD_GLOBAL              0 (lst)
LOAD_ATTR                1 (sort)
CALL_FUNCTION            0

可以看出,sort()函數(shù)被翻譯成了一條指令,執(zhí)行過(guò)程不會(huì)被打斷。

將lst的值加載到堆棧上

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

調(diào)用排序方法

即使lst.sort()需要幾個(gè)步驟,sort調(diào)用本身也是一個(gè)字節(jié)碼,因此不會(huì)被打斷。 我們可以得出結(jié)論,我們不需要鎖定sort()。 或者,請(qǐng)遵循一個(gè)簡(jiǎn)單的規(guī)則:始終鎖定共享可變狀態(tài)的讀寫。 畢竟,獲取Python中的threading.Lock花銷很低。
雖然GIL不能免除鎖的需要,但它確實(shí)意味著不需要細(xì)粒度的鎖定。 在像Java這樣的自由線程語(yǔ)言中,程序員努力在盡可能短的時(shí)間內(nèi)鎖定共享數(shù)據(jù),以減少線程爭(zhēng)用并允許最大并行度。 但是,由于線程無(wú)法并行運(yùn)行Python,因此細(xì)粒度鎖定沒(méi)有任何優(yōu)勢(shì)。 只要沒(méi)有線程在休眠時(shí)持有鎖,I / O或其他一些GIL丟棄操作,你應(yīng)該使用最粗糙,最簡(jiǎn)單的鎖。 無(wú)論如何,其他線程無(wú)法并行運(yùn)行。

并發(fā)提供更好的性能

在諸如網(wǎng)絡(luò)請(qǐng)求等I/O型的場(chǎng)景中,使用Python多線程可以帶來(lái)很高的性能提升,因?yàn)樵贗/O場(chǎng)景中,大多數(shù)線程都在等待I/O以進(jìn)行接下來(lái)的操作,所以即使單CPU,也能大大提高性能。比如下面這樣的代碼:

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()

如上所述,這些線程在等待通過(guò)HTTP獲取URL所涉及的每個(gè)套接字操作時(shí)丟棄GIL,因此它們比單個(gè)線程性能更高。

并行

如果你的任務(wù)一定要多線程才能更好的完成,那么,對(duì)于Python來(lái)說(shuō),多線程是不合適的,這種情況下,你得使用多進(jìn)程,因?yàn)槊總€(gè)進(jìn)程都是多帶帶的運(yùn)行環(huán)境,并且可以使用多核,但這會(huì)帶來(lái)更高的性能開(kāi)銷。下面的代碼就是使用多進(jìn)程來(lái)運(yùn)行任務(wù),每個(gè)進(jìn)程里只有一個(gè)線程。

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)

因?yàn)槊總€(gè)進(jìn)程都擁有多帶帶的GIL,所以這段代碼可以在多核CPU上并行執(zhí)行。

總結(jié)

由于Python GIL的存在,導(dǎo)致Python中一個(gè)進(jìn)程下的多個(gè)線程無(wú)法并行執(zhí)行,在I/O密集型的場(chǎng)景中,多線程依然能帶來(lái)比較好的性能,但是在CPU密集型的場(chǎng)景中,多線程無(wú)法帶來(lái)性能的提升。但同時(shí)也是由于GIL的存在,我們?cè)趩芜M(jìn)程中,線程安全也比較容易達(dá)到。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/43882.html

相關(guān)文章

  • 【須彌SUMERU】宜信分布式安全服務(wù)編排實(shí)踐

    摘要:通過(guò)可視化操作,將安全任務(wù)靈活編排成掃描流程。失效轉(zhuǎn)移失效轉(zhuǎn)移又稱故障切換,指系統(tǒng)中其中一項(xiàng)設(shè)備或服務(wù)失效而無(wú)法運(yùn)作時(shí),另一項(xiàng)設(shè)備或服務(wù)即可自動(dòng)接手原失效系統(tǒng)所執(zhí)行的工作,在須彌用于保障任務(wù)執(zhí)行過(guò)程中的執(zhí)行狀態(tài)。 概要 1.分布式安全服務(wù)編排概念 2.須彌(Sumeru)關(guān)鍵實(shí)現(xiàn)思路 3.應(yīng)用場(chǎng)景 前言 在筆者理解,安全防御的本質(zhì)之一是增加攻擊者的攻擊成本,尤其是時(shí)間成本,那么從防御...

    syoya 評(píng)論0 收藏0
  • 100 個(gè)基本 Python 面試問(wèn)題第二部分(41-60)

    摘要:回到目錄評(píng)論區(qū)抽粉絲送書啦歡迎大家在評(píng)論區(qū)提出意見(jiàn)和建議抽兩位幸運(yùn)兒送書,實(shí)物圖如下開(kāi)發(fā)從入門到精通內(nèi)容簡(jiǎn)介案例教學(xué)。 ? 作者主頁(yè):海擁 ? 作者簡(jiǎn)介:?CSDN...

    Tikitoo 評(píng)論0 收藏0
  • 【人情事故】做了3年銷售一事無(wú)成,轉(zhuǎn)行軟件測(cè)試成功后我就拿了8k!

    摘要:以下為我的真實(shí)案例以我真實(shí)案例分享,希望給更多決定重新開(kāi)始的人以鼓勵(lì)我已經(jīng)上班很久了,目前在中軟做軟件測(cè)試工程師,月薪,現(xiàn)在回想起來(lái),仍然慶幸我當(dāng)初的決定。 ?今天跟大家分享我的故事,或許你也曾像他那樣迷茫過(guò)。17年軟件工程專業(yè)專科畢業(yè)之后做了3年的銷售工作,最后決定還是再次提升專業(yè)技能,...

    _Zhao 評(píng)論0 收藏0
  • JavaScript 工作原理之六-WebAssembly 對(duì)比 JavaScript 及其使用場(chǎng)景

    摘要:現(xiàn)在,我們將會(huì)剖析的工作原理,而最重要的是它和在性能方面的比對(duì)加載時(shí)間,執(zhí)行速度,垃圾回收,內(nèi)存使用,平臺(tái)訪問(wèn),調(diào)試,多線程以及可移植性。目前,是專門圍繞和的使用場(chǎng)景設(shè)計(jì)的。目前不支持多線程。 原文請(qǐng)查閱這里,略有改動(dòng),本文采用知識(shí)共享署名 4.0 國(guó)際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請(qǐng)查閱這里。 這是 JavaScript 工作原理的第六章...

    jay_tian 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

B0B0

|高級(jí)講師

TA的文章

閱讀更多
最新活動(dòng)
閱讀需要支付1元查看
<