您好,登錄后才能下訂單哦!
隨著人工智能的興起,Python這門曾經小眾的編程語言可謂是煥發了第二春。
以tensorflow、pytorch等為主的機器學習/深度學習的開發框架大行其道,助推了python這門曾經以爬蟲見長(python粉別生氣)的編程語言在TIOBE編程語言排行榜上一路披荊斬棘,坐上前三甲的寶座,僅次于Java和C,將C++、JavaScript、PHP、C#等一眾勁敵斬落馬下。
當然,軒轅君向來是不提倡編程語言之間的競爭對比,每一門語言都有自己的優勢和劣勢,有自己應用的領域。
另一方面,TIOBE統計的數據也不能代表國內的實際情況,上面的例子只是側面反映了Python這門語言如今的流行程度。
Java 還是 Python
說回咱們的需求上來,如今在不少的企業中,同時存在Python研發團隊和Java研發團隊,Python團隊負責人工智能算法開發,而Java團隊負責算法工程化,將算法能力通過工程化包裝提供接口給更上層的應用使用。
可能大家要問了,為什么不直接用Java做AI開發呢?要弄兩個團隊。其實,現在包括TensorFlow在內的框架都逐漸開始支持Java平臺,用Java做AI開發也不是不行(軒轅君的前同事就已經在這樣做了),但限于歷史原因,做AI開發的人本就不多,而這一些人絕大部分都是Python技術棧入坑,Python的AI開發生態已經建設的相對完善,所以造成了在很多公司中算法團隊和工程化團隊使用不同的語言。
現在該拋出本文的重要問題:Java工程化團隊如何調用Python的算法能力?
答案基本上只有一個:Python通過Django/Flask等框架啟動一個Web服務,Java中通過Restful API與之進行交互
上面的方式的確可以解決問題,但隨之而來的就是性能問題。尤其是在用戶量上升后,大量并發接口訪問下,通過網絡訪問和Python的代碼執行速度將成為拖累整個項目的瓶頸。
當然,不差錢的公司可以用硬件堆出性能,一個不行,那就多部署幾個Python Web服務。
那除此之外,有沒有更實惠的解決方案呢?這就是這篇文章要討論的問題。
上面的性能瓶頸中,拖累執行速度的原因主要有兩個:
眾所周知,Python是一門解釋型腳本語言,一般來說,在執行速度上:
解釋型語言 < 中間字節碼語言 < 本地編譯型語言
自然而然,我們要努力的方向也就有兩個:
結合上面的兩個點,我們的目標也清晰起來:
將Python代碼轉換成Java可以直接本地調用的模塊
對于Java來說,能夠本地調用的有兩種:
其實我們通常所說的Python指的是CPython,也就是由C語言開發的解釋器來解釋執行。而除此之外,除了C語言,不少其他編程語言也能夠按照Python的語言規范開發出虛擬機來解釋執行Python腳本:
如果能夠在JVM中直接執行Python腳本,與Java業務代碼的交互自然是最簡單不過。但隨后的調研發現,這條路很快就被堵死了:
這條路行不通,那還有一條:把Python代碼轉換成Native代碼塊,Java通過JNI的接口形式調用。
先將Python源代碼轉換成C代碼,之后用GCC編譯C代碼為二進制模塊so/dll,接著進行一次Java Native接口封裝,使用Jar打包命令轉換成Jar包,然后Java便可以直接調用。
流程并不復雜,但要完整實現這個目標,有兩個關鍵問題需要解決:
1.Python代碼如何轉換成C代碼?
終于要輪到本文的主角登場了,將要用到的一個核心工具叫:Cython
請注意,這里的Cython和前面提到的CPython不是一回事。CPython狹義上是指C語言編寫的Python解釋器,是Windows、Linux下我們默認的Python腳本解釋器。
而Cython是Python的一個第三方庫,你可以通過pip install Cython
進行安裝。
官方介紹Cython是一個Python語言規范的超集,它可以將Python+C混合編碼的.pyx腳本轉換為C代碼,主要用于優化Python腳本性能或Python調用C函數庫。
聽上去有點復雜,也有點繞,不過沒關系,get一個核心點即可:Cython能夠把Python腳本轉換成C代碼
來看一個實驗:
# FileName: test.py def test_function(): print("this is print from python script")
將上述代碼通過Cython轉化,生成test.c,長這個樣子:
另外添加一個main.c,在其中實現C語言的main函數,并調用原python中的函數:
extern void test_function(); int main() { test_function(); return 0; }
輸出結果:
可以正常工作!
2.轉換后的C代碼如何包裝成JNI接口使用
1.Python源代碼
def logic(param): print('this is a logic function') # 接口函數,導出給Java Native的接口 def JNI_API_TestFunction(param): print("enter JNI_API_test_function") logic(param) print("leave JNI_API_test_function")
2.使用Cython工具轉換成C代碼
3.編譯生成動態庫
4.封裝為Jar包
準備一個JNI調用的Interface:JNITest.java
public class JNITest { native boolean Java_PkgName_module_initModule( ); native void Java_PkgName_module_uninitModule( ); native String Java_PkgName_module_TestFunction(String param); }
這里有3個native方法:
接口聲明文件+二進制動態庫文件準備就緒,開始打包:
jar -cvf JNITest.jar ./JNITest
5.Java調用
上面演示的案例只是一個單獨的py文件,而實際工作中,我們的項目通常是具有多個py文件,并且這些文件通常是構成了復雜的目錄層級,互相之間各種import關系,錯綜復雜。
Cython這個工具有一個最大的坑在于:經過其處理的文件代碼中會丟失代碼文件的目錄層級信息,如下圖所示,C.py轉換后的代碼和m/C.py生成的代碼沒有任何區別。
這就帶來一個非常大的問題:A.py或B.py代碼中如果有引用m目錄下的C.py模塊,目錄信息的丟失將導致二者在執行import m.C時報錯,找不到對應的模塊!
幸運的是,經過實驗表明,在上面的圖中,如果A、B、C三個模塊處于同一級目錄下時,import能夠正確執行。
軒轅君曾經嘗試閱讀Cython的源代碼,并進行修改,將目錄信息進行保留,使得生成后的C代碼仍然能夠正常import,但限于時間倉促,對Python解釋器機理了解不足,在一番嘗試之后選擇了放棄。
在這個問題上卡了很久,最終選擇了一種笨辦法:將樹形的代碼層級目錄展開成為平坦的目錄結構,就上圖中的例子而言,展開后的目錄結構變成了
A.py
B.py
m_C.py
單是這樣還不夠,還需要對A、B中引用到C的地方全部進行修正為對m_C的引用。
這看起來很簡單,但實際情況遠比這復雜,在Python中,import可不只有import這么簡單,有各種各樣復雜的形式:
import package import module import package.module import module.class / function import package.module.class / function import package.* import module.* from module import * from module import module from package import * from package import module from package.module import class / function ...
除此之外,在代碼中還可能存在直接通過模塊進行引用的寫法。
展開成為平坦結構的代價就是要處理上面所有的情況!軒轅君無奈之下只有出此下策,如果各位大佬有更好的解決方案還望不吝賜教。
Python轉換后的jar包開始用于實際生產中了,但隨后發現了一個問題:
每當Java并發數一上去之后,JVM總是不定時出現Crash
隨后分析崩潰信息發現,崩潰的地方正是在Native代碼中的Python轉換后的代碼中。
崩潰的烏云籠罩在頭上許久,冷靜下來思考:
為什么測試的時候正常沒有發現問題,上線之后才會崩潰?
再次翻看崩潰日志,發現在native代碼中,發生異常的地方總是在malloc分配內存的地方,難不成內存被破壞了?
又敏銳的發現測試的時候只是完成了功能性測試,并沒有進行并發壓力測試,而發生崩潰的場景總是在多并發環境中。多線程訪問JNI接口,那Native代碼將在多個線程上下文中執行。
猛地一個警覺:99%跟Python的GIL鎖有關系!
眾所周知,限于歷史原因,Python誕生于上世紀九十年代,彼時多線程的概念還遠遠沒有像今天這樣深入人心過,Python作為這個時代的產物一誕生就是一個單線程的產品。
雖然Python也有多線程庫,允許創建多個線程,但由于C語言版本的解釋器在內存管理上并非線程安全,所以在解釋器內部有一個非常重要的鎖在制約著Python的多線程,所以所謂多線程實際上也只是大家輪流來占坑。
原來GIL是由解釋器在進行調度管理,如今被轉成了C代碼后,誰來負責管理多線程的安全呢?
由于Python提供了一套供C語言調用的接口,允許在C程序中執行Python腳本,于是翻看這套API的文檔,看看能否找到答案。
幸運的是,還真被我找到了:
獲取GIL鎖:
釋放GIL鎖:
在JNI調用入口需要獲得GIL鎖,接口退出時需要釋放GIL鎖。
加入GIL鎖的控制后,煩人的Crash問題終于得以解決!
準備兩份一模一樣的py文件,同樣的一個算法函數,一個通過Flask Web接口訪問,(Web服務部署于本地127.0.0.1,盡可能減少網絡延時),另一個通過上述過程轉換成Jar包。
在Java服務中,分別調用兩個接口100次,整個測試工作進行10次,統計執行耗時:
上述測試中,為進一步區分網絡帶來的延遲和代碼執行本身的延遲,在算法函數的入口和出口做了計時,在Java執行接口調用前和獲得結果的地方也做了計時,這樣可以計算出算法執行本身的時間在整個接口調用過程中的占比。
本文提供了一種Java調用Python功能的新思路,僅供參考,其成熟度和穩定性還有待商榷,通過HTTP Restful接口訪問仍然是跨語言對接的首選。
到此這篇關于Python代碼一鍵轉Jar包及Java調用Python新姿勢的文章就介紹到這了,更多相關Python轉Jar包內容請搜索億速云以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持億速云!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。