自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何在 Go 中嵌入 Python

系統(tǒng) Linux
Datadog Agent 是一個 嵌入了 CPython 解釋器的普通 Go 二進(jìn)制文件,可以在任何時候按需執(zhí)行 Python 代碼。這個過程通過抽象層來透明化,使得你可以編寫慣用的 Go 代碼而底層運(yùn)行的是 Python。

如果你看一下 新的 Datadog Agent,你可能會注意到大部分代碼庫是用 Go 編寫的,盡管我們用來收集指標(biāo)的檢查仍然是用 Python 編寫的。這大概是因?yàn)?Datadog Agent 是一個 嵌入了 CPython 解釋器的普通 Go 二進(jìn)制文件,可以在任何時候按需執(zhí)行 Python 代碼。這個過程通過抽象層來透明化,使得你可以編寫慣用的 Go 代碼而底層運(yùn)行的是 Python。

在 Go 應(yīng)用程序中嵌入 Python 的原因有很多:

  • 它在過渡期間很有用;可以逐步將現(xiàn)有 Python 項(xiàng)目的部分遷移到新語言,而不會在此過程中丟失任何功能。
  • 你可以復(fù)用現(xiàn)有的 Python 軟件或庫,而無需用新語言重新實(shí)現(xiàn)。
  • 你可以通過加載去執(zhí)行常規(guī) Python 腳本來動態(tài)擴(kuò)展你軟件,甚至在運(yùn)行時也可以。

理由還可以列很多,但對于 Datadog Agent 來說,最后一點(diǎn)至關(guān)重要:我們希望做到無需重新編譯 Agent,或者說編譯任何內(nèi)容就能夠執(zhí)行自定義檢查或更改現(xiàn)有檢查。

嵌入 CPython 非常簡單,而且文檔齊全。解釋器本身是用 C 編寫的,并且提供了一個 C API 以編程方式來執(zhí)行底層操作,例如創(chuàng)建對象、導(dǎo)入模塊和調(diào)用函數(shù)。

在本文中,我們將展示一些代碼示例,我們將會在與 Python 交互的同時繼續(xù)保持 Go 代碼的慣用語,但在我們繼續(xù)之前,我們需要解決一個間隙:嵌入 API 是 C 語言,但我們的主要應(yīng)用程序是 Go,這怎么可能工作?

 

介紹 cgo

有 很多好的理由 說服你為什么不要在堆棧中引入 cgo,但嵌入 CPython 是你必須這樣做的原因。cgo 不是語言,也不是編譯器。它是 外部函數(shù)接口Foreign Function Interface(FFI),一種讓我們可以在 Go 中使用來調(diào)用不同語言(特別是 C)編寫的函數(shù)和服務(wù)的機(jī)制。

當(dāng)我們提起 “cgo” 時,我們實(shí)際上指的是 Go 工具鏈在底層使用的一組工具、庫、函數(shù)和類型,因此我們可以通過執(zhí)行 go build 來獲取我們的 Go 二進(jìn)制文件。下面是使用 cgo 的示例程序:

  1. package main
  2.  
  3. // #include <float.h>
  4. import "C"
  5. import "fmt"
  6.  
  7. func main() {
  8. fmt.Println("Max float value of float is", C.FLT_MAX)
  9. }
  10.  

在這種包含頭文件情況下,import "C" 指令上方的注釋塊稱為“序言preamble”,可以包含實(shí)際的 C 代碼。導(dǎo)入后,我們可以通過“C”偽包來“跳轉(zhuǎn)”到外部代碼,訪問常量 FLT_MAX。你可以通過調(diào)用 go build 來構(gòu)建,它就像普通的 Go 一樣。

如果你想查看 cgo 在這背后到底做了什么,可以運(yùn)行 go build -x。你將看到 “cgo” 工具將被調(diào)用以生成一些 C 和 Go 模塊,然后將調(diào)用 C 和 Go 編譯器來構(gòu)建目標(biāo)模塊,最后鏈接器將所有內(nèi)容放在一起。

你可以在 Go 博客 上閱讀更多有關(guān) cgo 的信息,該文章包含更多的例子以及一些有用的鏈接來做進(jìn)一步了解細(xì)節(jié)。

現(xiàn)在我們已經(jīng)了解了 cgo 可以為我們做什么,讓我們看看如何使用這種機(jī)制運(yùn)行一些 Python 代碼。

 

嵌入 CPython:一個入門指南

從技術(shù)上講,嵌入 CPython 的 Go 程序并沒有你想象的那么復(fù)雜。事實(shí)上,我們只需在運(yùn)行 Python 代碼之前初始化解釋器,并在完成后關(guān)閉它。請注意,我們在所有示例中使用 Python 2.x,但我們只需做很少的調(diào)整就可以應(yīng)用于 Python 3.x。讓我們看一個例子:

  1. package main
  2.  
  3. // #cgo pkg-config: python-2.7
  4. // #include <Python.h>
  5. import "C"
  6. import "fmt"
  7.  
  8. func main() {
  9. C.Py_Initialize()
  10. fmt.Println(C.GoString(C.Py_GetVersion()))
  11. C.Py_Finalize()
  12. }
  13.  

上面的例子做的正是下面 Python 代碼要做的事:

  1. import sys
  2. print(sys.version)

你可以看到我們在序言加入了一個 #cgo 指令;這些指令被會被傳遞到工具鏈,讓你改變構(gòu)建工作流程。在這種情況下,我們告訴 cgo 調(diào)用 pkg-config 來收集構(gòu)建和鏈接名為 python-2.7 的庫所需的標(biāo)志,并將這些標(biāo)志傳遞給 C 編譯器。如果你的系統(tǒng)中安裝了 CPython 開發(fā)庫和 pkg-config,你只需要運(yùn)行 go build 來編譯上面的示例。

回到代碼,我們使用 Py_Initialize() 和 Py_Finalize() 來初始化和關(guān)閉解釋器,并使用 Py_GetVersion C 函數(shù)來獲取嵌入式解釋器版本信息的字符串。

如果你想知道,所有我們需要放在一起調(diào)用 C 語言 Python API的 cgo 代碼都是模板代碼。這就是為什么 Datadog Agent 依賴 go-python 來完成所有的嵌入操作;該庫為 C API 提供了一個 Go 友好的輕量級包,并隱藏了 cgo 細(xì)節(jié)。這是另一個基本的嵌入式示例,這次使用 go-python:

  1. package main
  2.  
  3. import (
  4. python "github.com/sbinet/go-python"
  5. )
  6.  
  7. func main() {
  8. python.Initialize()
  9. python.PyRun_SimpleString("print 'hello, world!'")
  10. python.Finalize()
  11. }
  12.  

這看起來更接近普通 Go 代碼,不再暴露 cgo,我們可以在訪問 Python API 時來回使用 Go 字符串。嵌入式看起來功能強(qiáng)大且對開發(fā)人員友好,是時候充分利用解釋器了:讓我們嘗試從磁盤加載 Python 模塊。

在 Python 方面我們不需要任何復(fù)雜的東西,無處不在的“hello world” 就可以達(dá)到目的:

  1. # foo.py
  2. def hello():
  3. """
  4. Print hello world for fun and profit.
  5. """
  6. print "hello, world!"

Go 代碼稍微復(fù)雜一些,但仍然可讀:

  1. // main.go
  2. package main
  3.  
  4. import "github.com/sbinet/go-python"
  5.  
  6. func main() {
  7. python.Initialize()
  8. defer python.Finalize()
  9.  
  10. fooModule := python.PyImport_ImportModule("foo")
  11. if fooModule == nil {
  12. panic("Error importing module")
  13. }
  14.  
  15. helloFunc := fooModule.GetAttrString("hello")
  16. if helloFunc == nil {
  17. panic("Error importing function")
  18. }
  19.  
  20. // The Python function takes no params but when using the C api
  21. // we're required to send (empty) *args and **kwargs anyways.
  22. helloFunc.Call(python.PyTuple_New(0), python.PyDict_New())
  23. }
  24.  

構(gòu)建時,我們需要將 PYTHONPATH 環(huán)境變量設(shè)置為當(dāng)前工作目錄,以便導(dǎo)入語句能夠找到 foo.py 模塊。在 shell 中,該命令如下所示:

  1. $ go build main.go && PYTHONPATH=. ./main
  2. hello, world!

 

可怕的全局解釋器鎖

為了嵌入 Python 必須引入 cgo ,這是一種權(quán)衡:構(gòu)建速度會變慢,垃圾收集器不會幫助我們管理外部系統(tǒng)使用的內(nèi)存,交叉編譯也很難。對于一個特定的項(xiàng)目來說,這些問題是否是可以爭論的,但我認(rèn)為有一些不容商量的問題:Go 并發(fā)模型。如果我們不能從 goroutine 中運(yùn)行 Python,那么使用 Go 就沒有意義了。

在處理并發(fā)、Python 和 cgo 之前,我們還需要知道一些事情:它就是全局解釋器鎖Global Interpreter Lock,即 GIL。GIL 是語言解釋器(CPython 就是其中之一)中廣泛采用的一種機(jī)制,可防止多個線程同時運(yùn)行。這意味著 CPython 執(zhí)行的任何 Python 程序都無法在同一進(jìn)程中并行運(yùn)行。并發(fā)仍然是可能的,鎖是速度、安全性和實(shí)現(xiàn)簡易性之間的一個很好的權(quán)衡,那么,當(dāng)涉及到嵌入時,為什么這會造成問題呢?

當(dāng)一個常規(guī)的、非嵌入式的 Python 程序啟動時,不涉及 GIL 以避免鎖定操作中的無用開銷;在某些 Python 代碼首次請求生成線程時 GIL 就啟動了。對于每個線程,解釋器創(chuàng)建一個數(shù)據(jù)結(jié)構(gòu)來存儲當(dāng)前的相關(guān)狀態(tài)信息并鎖定 GIL。當(dāng)線程完成時,狀態(tài)被恢復(fù),GIL 被解鎖,準(zhǔn)備被其他線程使用。

當(dāng)我們從 Go 程序運(yùn)行 Python 時,上述情況都不會自動發(fā)生。如果沒有 GIL,我們的 Go 程序可以創(chuàng)建多個 Python 線程,這可能會導(dǎo)致競爭條件,從而導(dǎo)致致命的運(yùn)行時錯誤,并且很可能出現(xiàn)分段錯誤導(dǎo)致整個 Go 應(yīng)用程序崩潰。

解決方案是在我們從 Go 運(yùn)行多線程代碼時顯式調(diào)用 GIL;代碼并不復(fù)雜,因?yàn)?C API 提供了我們需要的所有工具。為了更好地暴露這個問題,我們需要寫一些受 CPU 限制的 Python 代碼。讓我們將這些函數(shù)添加到前面示例中的 foo.py 模塊中:

  1. # foo.py
  2. import sys
  3.  
  4. def print_odds(limit=10):
  5. """
  6. Print odds numbers < limit
  7. """
  8. for i in range(limit):
  9. if i%2:
  10. sys.stderr.write("{}\n".format(i))
  11.  
  12. def print_even(limit=10):
  13. """
  14. Print even numbers < limit
  15. """
  16. for i in range(limit):
  17. if i%2 == 0:
  18. sys.stderr.write("{}\n".format(i))
  19.  

我們將嘗試從 Go 并發(fā)打印奇數(shù)和偶數(shù),使用兩個不同的 goroutine(因此涉及線程):

  1. package main
  2.  
  3. import (
  4. "sync"
  5.  
  6. "github.com/sbinet/go-python"
  7. )
  8.  
  9. func main() {
  10. // The following will also create the GIL explicitly
  11. // by calling PyEval_InitThreads(), without waiting
  12. // for the interpreter to do that
  13. python.Initialize()
  14.  
  15. var wg sync.WaitGroup
  16. wg.Add(2)
  17.  
  18. fooModule := python.PyImport_ImportModule("foo")
  19. odds := fooModule.GetAttrString("print_odds")
  20. even := fooModule.GetAttrString("print_even")
  21.  
  22. // Initialize() has locked the the GIL but at this point we don't need it
  23. // anymore. We save the current state and release the lock
  24. // so that goroutines can acquire it
  25. state := python.PyEval_SaveThread()
  26.  
  27. go func() {
  28. _gstate := python.PyGILState_Ensure()
  29. odds.Call(python.PyTuple_New(0), python.PyDict_New())
  30. python.PyGILState_Release(_gstate)
  31.  
  32. wg.Done()
  33. }()
  34.  
  35. go func() {
  36. _gstate := python.PyGILState_Ensure()
  37. even.Call(python.PyTuple_New(0), python.PyDict_New())
  38. python.PyGILState_Release(_gstate)
  39.  
  40. wg.Done()
  41. }()
  42.  
  43. wg.Wait()
  44.  
  45. // At this point we know we won't need Python anymore in this
  46. // program, we can restore the state and lock the GIL to perform
  47. // the final operations before exiting.
  48. python.PyEval_RestoreThread(state)
  49. python.Finalize()
  50. }
  51.  

在閱讀示例時,你可能會注意到一個模式,該模式將成為我們運(yùn)行嵌入式 Python 代碼的習(xí)慣寫法:

  1. 保存狀態(tài)并鎖定 GIL。
  2. 執(zhí)行 Python。
  3. 恢復(fù)狀態(tài)并解鎖 GIL。

代碼應(yīng)該很簡單,但我們想指出一個微妙的細(xì)節(jié):請注意,盡管借用了 GIL 執(zhí)行,有時我們通過調(diào)用 PyEval_SaveThread() 和 PyEval_RestoreThread() 來操作 GIL,有時(查看 goroutines 里面)我們對 PyGILState_Ensure() 和 PyGILState_Release() 來做同樣的事情。

我們說過當(dāng)從 Python 操作多線程時,解釋器負(fù)責(zé)創(chuàng)建存儲當(dāng)前狀態(tài)所需的數(shù)據(jù)結(jié)構(gòu),但是當(dāng)同樣的事情發(fā)生在 C API 時,我們來負(fù)責(zé)處理。

當(dāng)我們用 go-python 初始化解釋器時,我們是在 Python 上下文中操作的。因此,當(dāng)調(diào)用 PyEval_InitThreads() 時,它會初始化數(shù)據(jù)結(jié)構(gòu)并鎖定 GIL。我們可以使用 PyEval_SaveThread() 和 PyEval_RestoreThread() 對已經(jīng)存在的狀態(tài)進(jìn)行操作。

在 goroutines 中,我們從 Go 上下文操作,我們需要顯式創(chuàng)建狀態(tài)并在完成后將其刪除,這就是 PyGILState_Ensure() 和 PyGILState_Release() 為我們所做的。

 

釋放 Gopher

在這一點(diǎn)上,我們知道如何處理在嵌入式解釋器中執(zhí)行 Python 的多線程 Go 代碼,但在 GIL 之后,另一個挑戰(zhàn)即將來臨:Go 調(diào)度程序。

當(dāng)一個 goroutine 啟動時,它被安排在可用的 GOMAXPROCS 線程之一上執(zhí)行,參見此處 可了解有關(guān)該主題的更多詳細(xì)信息。如果一個 goroutine 碰巧執(zhí)行了系統(tǒng)調(diào)用或調(diào)用 C 代碼,當(dāng)前線程會將線程隊(duì)列中等待運(yùn)行的其他 goroutine 移交給另一個線程,以便它們有更好的機(jī)會運(yùn)行; 當(dāng)前 goroutine 被暫停,等待系統(tǒng)調(diào)用或 C 函數(shù)返回。當(dāng)這種情況發(fā)生時,線程會嘗試恢復(fù)暫停的 goroutine,但如果這不可能,它會要求 Go 運(yùn)行時找到另一個線程來完成 goroutine 并進(jìn)入睡眠狀態(tài)。 goroutine 最后被安排給另一個線程,它就完成了。

考慮到這一點(diǎn),讓我們看看當(dāng)一個 goroutine 被移動到一個新線程時,運(yùn)行一些 Python 代碼的 goroutine 會發(fā)生什么:

  1. 我們的 goroutine 啟動,執(zhí)行 C 調(diào)用并暫停。GIL 被鎖定。
  2. 當(dāng) C 調(diào)用返回時,當(dāng)前線程嘗試恢復(fù) goroutine,但失敗了。
  3. 當(dāng)前線程告訴 Go 運(yùn)行時尋找另一個線程來恢復(fù)我們的 goroutine。
  4. Go 調(diào)度器找到一個可用線程并恢復(fù) goroutine。
  5. goroutine 快完成了,并在返回之前嘗試解鎖 GIL。
  6. 當(dāng)前狀態(tài)中存儲的線程 ID 來自原線程,與當(dāng)前線程的 ID 不同。
  7. 崩潰!

所幸,我們可以通過從 goroutine 中調(diào)用運(yùn)行時包中的 LockOSThread 函數(shù)來強(qiáng)制 Go runtime 始終保持我們的 goroutine 在同一線程上運(yùn)行:

  1. go func() {
  2. runtime.LockOSThread()
  3.  
  4. _gstate := python.PyGILState_Ensure()
  5. odds.Call(python.PyTuple_New(0), python.PyDict_New())
  6. python.PyGILState_Release(_gstate)
  7. wg.Done()
  8. }()

這會干擾調(diào)度器并可能引入一些開銷,但這是我們愿意付出的代價。

結(jié)論

為了嵌入 Python,Datadog Agent 必須接受一些權(quán)衡:

  • cgo 引入的開銷。
  • 手動處理 GIL 的任務(wù)。
  • 在執(zhí)行期間將 goroutine 綁定到同一線程的限制。

為了能方便在 Go 中運(yùn)行 Python 檢查,我們很樂意接受其中的每一項(xiàng)。但通過意識到這些權(quán)衡,我們能夠最大限度地減少它們的影響,除了為支持 Python 而引入的其他限制,我們沒有對策來控制潛在問題:

  • 構(gòu)建是自動化和可配置的,因此開發(fā)人員仍然需要擁有與 go build 非常相似的東西。
  • Agent 的輕量級版本,可以使用 Go 構(gòu)建標(biāo)簽,完全剝離 Python 支持。
  • 這樣的版本僅依賴于在 Agent 本身硬編碼的核心檢查(主要是系統(tǒng)和網(wǎng)絡(luò)檢查),但沒有 cgo 并且可以交叉編譯。

我們將在未來重新評估我們的選擇,并決定是否仍然值得保留 cgo;我們甚至可以重新考慮整個 Python 是否仍然值得,等待 Go 插件包 成熟到足以支持我們的用例。但就目前而言,嵌入式 Python 運(yùn)行良好,從舊代理過渡到新代理再簡單不過了。

你是一個喜歡混合不同編程語言的多語言者嗎?你喜歡了解語言的內(nèi)部工作原理以提高你的代碼性能嗎? 

責(zé)任編輯:龐桂玉 來源: Linux中國
相關(guān)推薦

2023-06-15 13:01:07

JavaPythonJavaScript

2013-03-04 14:35:05

WordPressEdge AnimatHTML5

2015-07-06 09:59:56

JavaScript私有成員

2023-11-26 19:06:13

GO測試

2021-11-08 10:58:08

變量依賴圖排序

2022-09-19 11:42:21

Go優(yōu)化CPU

2010-08-10 15:55:20

FlexHTML頁面

2020-09-15 10:45:06

PythonPyQt5Matplotlib

2022-06-22 09:56:19

PythonMySQL數(shù)據(jù)庫

2021-07-02 07:18:19

Goresults通道類型

2020-07-06 15:50:41

Python文件Linux

2018-11-05 14:53:14

Go函數(shù)代碼

2025-01-21 15:20:14

2021-07-02 20:37:19

Python代碼SRP

2009-02-17 23:51:57

Linux程序登錄界面

2010-08-10 14:08:09

Flex嵌入字體

2016-11-03 18:39:39

JavaMySQL

2021-01-18 17:23:30

代碼調(diào)試VS Code

2023-09-01 08:19:21

Flask

2022-09-20 08:43:37

Go編程語言Web
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號