Python中的GIL和深浅拷贝
Python中的GIL和深淺拷貝
文章目錄
- Python中的GIL和深淺拷貝
- 一、GIL全局解釋器鎖
- 1.引入
- 2、GIL
- 3、Python GIL底層實現原理
- 4、計算密集型和IO密集型
- 5、解決GIL問題的方案:
- 6、線程釋放GIL鎖的情況:
- 7、GIL對python多線程的影響?闡明多線程程序是否可比單線程性能有提升,并解釋原因。
- 二、深淺拷貝
- 1、 淺拷貝
- 2. 深拷貝
- 3、拷貝的其他方式
- 4. 注意點
- 5、copy.copy和copy.deepcopy的區別
- 6、總結
一、GIL全局解釋器鎖
1.引入
GIL,中文譯為全局解釋器鎖。在講解 GIL 之前,首先通過一個例子來直觀感受一下 GIL 在 Python 多線程程序運行的影響。
首先運行如下程序:
import time start = time.clock() def CountDown(n):while n > 0:n -= 1 CountDown(100000) print("Time used:",(time.clock() - start)) 運行結果為: Time used: 0.0039529000000000005在我們的印象中,使用多個(適量)線程是可以加快程序運行效率的,因此可以嘗試將上面程序改成如下方式:
import time from threading import Thread start = time.clock() def CountDown(n):while n > 0:n -= 1 t1 = Thread(target=CountDown, args=[100000 // 2]) t2 = Thread(target=CountDown, args=[100000 // 2]) t1.start() t2.start() t1.join() t2.join() print("Time used:",(time.clock() - start))運行結果為: Time used: 0.006673- 可以看到,此程序中使用了 2 個線程來執行和上面代碼相同的工作,但從輸出結果中可以看到,運行效率非但沒有提高,反而降低了。
- 如果使用更多線程進行嘗試,會發現其運行效率和 2 個線程效率幾乎一樣(本機器測試使用 4 個線程,其執行效率約為 0.005)
- 是不是和你猜想的結果不一樣?事實上,得到這樣的結果是肯定的,因為 GIL 限制了 Python 多線程的性能不會像我們預期的那樣。
2、GIL
GIL 是最流程的 CPython 解釋器(平常稱為 Python)中的一個技術術語,中文譯為全局解釋器鎖,其本質上類似操作系統的 Mutex。GIL 的功能是:在 CPython 解釋器中執行的每一個 Python 線程,都會先鎖住自己,以阻止別的線程執行。
當然,CPython 不可能容忍一個線程一直獨占解釋器,它會輪流執行 Python 線程。這樣一來,用戶看到的就是“偽”并行,即 Python 線程在交替執行,來模擬真正并行的線程。
有讀者可能會問,既然 CPython 能控制線程偽并行,為什么還需要 GIL 呢?其實,這和 CPython 的底層內存管理有關。
CPython 使用引用計數來管理內容,所有 Python 腳本中創建的實例,都會配備一個引用計數,來記錄有多少個指針來指向它。當實例的引用計數的值為 0 時,會自動釋放其所占的內存。
舉個例子,看如下代碼:
>>> import sys >>> a = [] >>> b = a >>> sys.getrefcount(a) 3- 可以看到,a 的引用計數值為 3,因為有 a、b 和作為參數傳遞的 getrefcount 都引用了一個空列表。
- 假設有兩個 Python 線程同時引用 a,那么雙方就都會嘗試操作該數據,很有可能造成引用計數的條件競爭,導致引用計數只增加1(實際應增加 2)
- 這造成的后果是,當第一個線程結束時,會把引用計數減少 1,此時可能已經達到釋放內存的條件(引用計數為 0),當第 2 個線程再次視圖訪問 a 時,就無法找到有效的內存了。
- 所以,CPython 引進 GIL,可以最大程度上規避類似內存管理這樣復雜的競爭風險問題。
3、Python GIL底層實現原理
- 上面這張圖,就是 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3
輪流執行 - 每一個線程在開始執行時,都會鎖住 GIL,以阻止別的線程執行;
- 同樣的,每一個線程執行完一段后,會釋放GIL,以允許別的線程開始利用資源。
- 讀者可能會問,為什么 Python 線程會去主動釋放 GIL 呢?畢竟,如果僅僅要求 Python 線程在開始執行時鎖住 GIL,且永遠不去釋放 GIL,那別的線程就都沒有運行的機會。
- 其實,CPython 中還有另一個機制,叫做間隔式檢查(check_interval),意思是 CPython 解釋器會去輪詢檢查線程 GIL 的鎖住情況,每隔一段時間,Python 解釋器就會強制當前線程去釋放 GIL,這樣別的線程才能有執行的機會。
- 注意,不同版本的 Python,其間隔式檢查的實現方式并不一樣。早期的 Python 是 100 個刻度(大致對應了 1000 個字節碼);
- 而 Python 3 以后,間隔時間大致為 15 毫秒。當然,我們不必細究具體多久會強制釋放 GIL,讀者只需要明白,CPython 解釋器會在一個“合理”的時間范圍內釋放 GIL 就可以了。
整體來說,每一個 Python 線程都是類似這樣循環的封裝,來看下面這段代碼:
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 ... */} }- 從這段代碼中可以看出,每個 Python 線程都會先檢查 ticker 計數。只有在 ticker 大于 0的情況下,線程才會去執行自己的代碼。
- Python GIL不能絕對保證線程安全
- 注意,有了 GIL,并不意味著 Python 程序員就不用去考慮線程安全了,因為即便 GIL 僅允許一個 Python 線程執行,但別忘了 Python 還有 check interval 這樣的搶占機制。
比如,運行如下代碼:
import threading n = 0 def foo():global nn += 1 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,但有時也會打印 99 或者 98,原因在于 n+=1 這一句代碼讓線程并不安全。如果去翻譯 foo 這個函數的字節碼就會發現,它實際上是由下面四行字節碼組成:
>>> import dis >>> dis.dis(foo) LOAD_GLOBAL 0 (n) LOAD_CONST 1 (1) INPLACE_ADD STORE_GLOBAL 0 (n)而這四行字節碼中間都是有可能被打斷的!所以,千萬別以為有了 GIL 程序就不會產生線程問題,我們仍然需要注意線程安全。
4、計算密集型和IO密集型
遇到IO阻塞時,正在執行的線程會暫時釋放GIL鎖,這時其它線程會利用這個空隙時間,執行自己的代碼,因此多線程抓取比單線程抓取性能要好。
- 計算密集型:要進行大量的數值計算,例如進行上億的數字計算、計算圓周率、對視頻進行高清解碼等等。這種計算密集型任務雖然也可以用多任務完成,但是花費的主要時間在任務切換的時間,此時CPU執行任務的效率比較低。
- IO密集型:涉及到網絡請求(time.sleep())、磁盤IO的任務都是IO密集型任務,這類任務的特點是CPU消耗很少,任務的大部分時間都在等待IO操作完成(因為IO的速度遠遠低于CPU和內存的速度)。對于IO密集型任務,任務越多,CPU效率越高,但也有一個限度。
5、解決GIL問題的方案:
1.使用其它語言,例如C,Java
2.使用其它解釋器,如java的解釋器jython
3.使用多進程
6、線程釋放GIL鎖的情況:
1.在IO操作等可能會引起阻塞的system call之前,可以暫時釋放GIL,但在執行完畢后,必須重新獲取GIL。
2.Python 3.x使用計時器(執行時間達到閾值后,當前線程釋放GIL)或Python 2.x,tickets計數達到100。
7、GIL對python多線程的影響?闡明多線程程序是否可比單線程性能有提升,并解釋原因。
二、深淺拷貝
1、 淺拷貝
淺拷貝是對于一個對象的頂層拷貝
通俗的理解是:拷貝了引用,并沒有拷貝內容
2. 深拷貝
深拷貝是對于一個對象所有層次的拷貝(遞歸)
3、拷貝的其他方式
- 分片表達式可以賦值一個序列
- 字典的copy方法可以拷貝一個字典
4. 注意點
淺拷貝對不可變類型和可變類型的copy不同
copy.copy對于可變類型,會進行淺拷貝
copy.copy對于不可變類型,不會拷貝,僅僅是指向
5、copy.copy和copy.deepcopy的區別
- copy.copy
- copy.deepcopy
6、總結
- 深淺拷貝都是對源對象的復制,占用不同的內存空間
- 如果源對象只有一級目錄的話,源做任何改動,不影響深淺拷貝對象
- 如果源對象不止一級目錄的話,源做任何改動,都要影響淺拷貝,但不影響深拷貝
- 序列對象的切片其實是淺拷貝,即只拷貝頂級的對象
總結
以上是生活随笔為你收集整理的Python中的GIL和深浅拷贝的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python中的select、epoll
- 下一篇: Python中菱形继承的MRO顺序及pr