python多线程gil_Python 多线程、多进程 (一)之 源码执行流程、GIL
一、python程序的運行原理
許多時候,在執行一個python文件的時候,會發現在同一目錄下會出現一個__pyc__文件夾(python3)或者.pyc后綴(python2)的文件
Python在執行時,首先會將.py文件中的源代碼編譯成Python的byte code(字節碼),然后再由Python Virtual Machine(Python虛擬機)來執行這些編譯好的byte code。
1、執行流程源代碼.py?——(編譯處理)——>字節碼.pyc?———>python虛擬機——(編譯)——>程序
2、編譯
執行 python demo.py 后,將會啟動 Python 的解釋器,然后將 demo.py 編譯成一個字節碼對象 PyCodeObject。在 Python 的世界中,一切都是對象,函數也是對象,類型也是對象,類也是對象(類屬于自定義的類型,在 Python 2.2 之前,int, dict 這些內置類型與類是存在不同的,在之后才統一起來,全部繼承自 object),甚至連編譯出來的字節碼也是對象,.pyc 文件是字節碼對象(PyCodeObject)在硬盤上的表現形式。
在運行期間,編譯結果也就是 PyCodeObject 對象,只會存在于內存中,而當這個模塊的 Python 代碼執行完后,就會將編譯結果保存到了 pyc 文件中,這樣下次就不用編譯,直接加載到內存中。pyc 文件只是 PyCodeObject 對象在硬盤上的表現形式。
這個 PyCodeObject 對象包含了 Python 源代碼中的字符串,常量值,以及通過語法解析后編譯生成的字節碼指令。PyCodeObject 對象還會存儲這些字節碼指令與原始代碼行號的對應關系,這樣當出現異常時,就能指明位于哪一行的代碼。
3、pyc文件
一個 pyc 文件包含了三部分信息:Python 的 magic number、pyc 文件創建的時間信息,以及 PyCodeObject 對象。
magic number 是 Python 定義的一個整數值。一般來說,不同版本的 Python 實現都會定義不同的 magic number,這個值是用來保證 Python 兼容性的。比如要限制由低版本編譯的 pyc 文件不能讓高版本的 Python 程序來執行,只需要檢查 magic number 不同就可以了。由于不同版本的 Python 定義的字節碼指令可能會不同,如果不做檢查,執行的時候就可能出錯。
4、字節碼指令
為什么 pyc 文件也稱作字節碼文件?因為這些文件存儲的都是一些二進制的字節數據,而不是能讓人直觀查看的文本數據。
Python 標準庫提供了用來生成代碼對應字節碼的工具 dis。dis 提供一個名為 dis 的方法,這個方法接收一個 code 對象,然后會輸出 code 對象里的字節碼指令信息。#?test1.pyimport?dis
def?add(a):????a?=?a+1
return?a
print(dis.dis(add))#?輸出
10???????????0?LOAD_FAST????????????????0?(a)??????????????3?LOAD_CONST???????????????1?(1)??????????????6?BINARY_ADD??????????????7?STORE_FAST???????????????0?(a)?11??????????10?LOAD_FAST????????????????0?(a)?????????????13?RETURN_VALUE
5、python虛擬機
demo.py 被編譯后,接下來的工作就交由 Python 虛擬機來執行字節碼指令了。Python 虛擬機會從編譯得到的 PyCodeObject 對象中依次讀入每一條字節碼指令,并在當前的上下文環境中執行這條字節碼指令。我們的程序就是通過這樣循環往復的過程才得以執行。
二、進程線程
1、進程
程序僅僅只是一堆代碼而已,而進程指的是程序的運行過程。需要強調的是:同一個程序執行兩次,那也是兩個進程。
進程:資源管理單位(容器)。
線程:最小執行單位,管理線程的是進程。
進程就是一個程序在一個數據集上的一次動態執行過程。進程一般由程序、數據集、進程控制塊三部分組成。我們編寫的程序用來描述進程要完成哪些功能以及如何完成;數據集則是程序在執行過程中所需要使用的資源;進程控制塊用來記錄進程的外部特征,描述進程的執行變化過程,系統可以利用它來控制和管理進程,它是系統感知進程存在的唯一標志。
2、線程
線程的出現是為了降低上下文切換的消耗,提高系統的并發性,并突破一個進程只能干一樣事的缺陷,使到進程內并發成為可能。
線程也叫輕量級進程,它是一個基本的CPU執行單元,也是程序執行過程中的最小單元,由線程ID、程序計數器、寄存器集合和堆棧共同組成。線程的引入減小了程序并發執行時的開銷,提高了操作系統的并發性能。線程沒有自己的系統資源。
3、線程與進程關系
在傳統操作系統中,每個進程有一個地址空間,而且默認就有一個控制線程。
多線程(即多個控制線程)的概念是,在一個進程中存在多個控制線程,控制該進程的地址空間。
進程只是用來把資源集中到一起(進程只是一個資源單位,或者說資源集合),而線程才是cpu上的執行單位。
進程和線程的關系:
(1)一個線程只能屬于一個進程,而一個進程可以有多個線程,但至少有一個線程。
(2)資源分配給進程,同一進程的所有線程共享該進程的所有資源。
(3)CPU分給線程,即真正在CPU上運行的是線程。
4、串行,并行與并發
比較重要的就是,無論是并行還是并發,在用戶看來都是'同時'運行的,而一個cpu同一時刻只能執行一個任務。
并行:同時運行,只有具備多個cpu才能實現并行。
并發:是偽并行,即看起來是同時運行,單個cpu+多道技術。
多道技術:內存中同時存入多道(多個)程序,cpu從一個進程快速切換到另外一個,并且切換時間十分短暫,所以給人的感覺是我可以邊打游戲邊聽歌。多個程序并行執行,其實是偽并行即并發。
阮一峰關于線程進程更形象介紹傳送門:
5、同步與異步
同步就是指一個進程在執行某個請求的時候,若該請求需要一段時間才能返回信息,那么這個進程將會一直等待下去,直到收到返回信息才繼續執行下去;異步是指進程不需要一直等下去,而是繼續執行下面的操作,不管其他進程的狀態。當有消息返回時系統會通知進程進行處理,這樣可以提高執行的效率。
打電話時就是同步通信,發短息時就是異步通信。
6、生產者與消費者
生產者消費者模式:某些模塊負責生產數據,這些數據由其他模塊來負責處理(此處的模塊可能是:函數、線程、進程等)。產生數據的模塊稱為生產者,而處理數據的模塊稱為消費者。在生產者與消費者之間的緩沖區稱之為倉庫。生產者負責往倉庫運輸>商品,而消費者負責從倉庫里取出商品,這就構成了生產者消費者模式。
比如在網絡I/O的時候,一個對象負責請求數據,另一個對象負責處理數據,中間就需要一個容器來負責數據的緩沖,平衡兩個對象之間的處理速度的協調。
優點:解耦:由于兩個對象之間的方法獨立,數據的獲取只需要通過接口的調用,所以兩者的依賴性低,可重用性高
平衡了生產力與消費力,就是生產者一直不停的生產,消費者可以不停的消費,因為二者不再是直接溝通的,而是通過數據緩沖區溝通的。生產者的數據直接丟入緩沖區,消費者直接從緩沖區那數據,就不會造成因為數據因為過剩造成生產者阻塞,或者數據過少消費者阻塞的問題
舉例男生:我負責掙錢養家,你呢?
女生:我負責貌美如花。
男生:那如果錢不夠?
女生:那就等錢夠了再娶我,我等著!
男生:如果錢太多呢?
女生:那就存著,我慢慢花!
從上面可以抽象出三個對象,生產者(男生),消費者(女生),數據(錢),而數據暫存到哪,一般是為了解決加鎖問題,放到隊列而不是簡單的容器類型。
三、全局解釋器鎖
全局解釋器鎖(Global Interpreter Lock):簡稱GIL,多進程(mutilprocess) 和 多線程(threading)的目的是用來被多顆CPU進行訪問, 提高程序的執行效率。 但是多線程之間數據完整性和狀態同步是一個很大的問題,所以在python內部存在一種機制(GIL),在多線程時同一時刻只允許一個線程來訪問CPU,也就是不同線程對共享資源的互斥。 在一個線程擁有了解釋器的訪問權之后,其他的所有線程都必須等待它釋放解釋器的訪問權,即使這些線程的下一條指令并不會互相影響。GIL 并不是Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。因為CPython是大部分環境下默認的Python執行環境。所以在把GIL之殤歸結給Python是不對的。GIL并不是Python的特性,Python完全可以不依賴于GIL。例如Jython(java編寫的python解釋器)就不會存在GIL。python中一個線程對應于c語言中的一個線程
GIL使得同一個時刻只有一個線程在一個CPU上執行字節碼, 無法將多個線程映射到多個cpu上執行,因此python是無法利用多核CPU實現多線程的
大量的第三方包都是基于CPython編寫的,所以短期內想把GIL去掉不太可能
1、GIL優缺點
缺點:多處理器退化為單處理器;
優點:避免大量的加鎖解鎖操作
2、GIL釋放
要實現python的多線程就需要借助標準庫threading#?test2.pyimport?threading
total?=?0def?add():
#?連續執行total的加操作
global?total????for?i?in?range(1000000):
total?+=?1def?reduce():
#?連續執行total的減操作
global?total????for?i?in?range(1000000):
total?-=?1#?創建兩個線程thread1?=?threading.Thread(target=add)
thread2?=?threading.Thread(target=desc)#?線程開始thread1.start()
thread2.start()#?線程結束thread1.join()
thread2.join()
print(total)
使用total作為標志,通過total的值判斷線程的實現。
如果實現GIL沒有釋放的的話,那么兩個線程先后完成,打印結果應該是0,而實際打印結果卻不是0,并且每次打印結果也都不一致,說明實現了GIL主動釋放掉了。
total變量是一個全局變量,其實在add與reduce內部的賦值語句total+=1與total-=1時,高級語言每一條語句在CPU上執行的時候又被對應成許多語句,比如total+=1對應成x1=total+1,total=x1,而total-=1被對應成x2=toal-1,total=x2,每一個x都是函數內部的局部變量。
注:可以對應字節碼指令來理解,可以參照上面GIL中的實例使用dis模塊獲取字節碼查看,PVM(python虛擬機)其實執行的也就是字節碼指令。。
正常執行:初始total=0add:x1?=?total?+1??#?x1?=?1total?=?x1
total?=?1reduce:x2?=?total-1??#?x2?=?0total?=?x2
total?=?0
最終循環一次結果0
正常應該是無論多少次循環結果total都是0
多線程共享變量,兩個線程交替占用cpu,:total=0add:x1?=?total?+?1??#?x1?=?1reduce:x2?=?total?-?1??#?x2?=?-1total?=?x2?#?total?=?-1add:total?=?x1
total?=1
最終循環結果為1
只要進行足夠多的循環,total的值就會出現不可預計的結果
所以,在修改total值的時候,需要多條語句。所以我覺得上面的例子可以這么理解:就是當一個線程在執行的時候也就是PVM在執行字節碼指令,當字節碼指令到達一定數目(ticks專門計數),此線程不再擁有GIL(釋放GIL,release)并且釋放CPU資源,但是其他的線程又過來搶,這個線程沒搶過它,GIL就這樣別搶走了,CPU資源就暫時交給其他的線程了(嗯,天道有輪回,下次我還會搶回來的)。因此,線程之間共享數據最大的危險在于多個線程同時改一個變量。所以在進行python多線程變成的時候,一般會進行細粒度的自定義加鎖,以保證安全性。
問題:GIL什么時候會釋放?執行的字節碼行數到達一定閾值
通過時間片劃分,到達一定時間閾值
在遇到IO操作時,主動釋放
總結
以上是生活随笔為你收集整理的python多线程gil_Python 多线程、多进程 (一)之 源码执行流程、GIL的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 赚钱 知乎_爬虫实战:抓取
- 下一篇: sqldataadapter.fill