成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

如何在 Go 中嵌入 Python

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

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

在 Go 應用程序中嵌入 Python 的原因有很多:

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

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

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

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

 

介紹 cgo

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

當我們提起 “cgo” 時,我們實際上指的是 Go 工具鏈在底層使用的一組工具、庫、函數和類型,因此我們可以通過執行 go build 來獲取我們的 Go 二進制文件。下面是使用 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”,可以包含實際的 C 代碼。導入后,我們可以通過“C”偽包來“跳轉”到外部代碼,訪問常量 FLT_MAX。你可以通過調用 go build 來構建,它就像普通的 Go 一樣。

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

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

現在我們已經了解了 cgo 可以為我們做什么,讓我們看看如何使用這種機制運行一些 Python 代碼。

 

嵌入 CPython:一個入門指南

從技術上講,嵌入 CPython 的 Go 程序并沒有你想象的那么復雜。事實上,我們只需在運行 Python 代碼之前初始化解釋器,并在完成后關閉它。請注意,我們在所有示例中使用 Python 2.x,但我們只需做很少的調整就可以應用于 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 指令;這些指令被會被傳遞到工具鏈,讓你改變構建工作流程。在這種情況下,我們告訴 cgo 調用 pkg-config 來收集構建和鏈接名為 python-2.7 的庫所需的標志,并將這些標志傳遞給 C 編譯器。如果你的系統中安裝了 CPython 開發庫和 pkg-config,你只需要運行 go build 來編譯上面的示例。

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

如果你想知道,所有我們需要放在一起調用 C 語言 Python API的 cgo 代碼都是模板代碼。這就是為什么 Datadog Agent 依賴 go-python 來完成所有的嵌入操作;該庫為 C API 提供了一個 Go 友好的輕量級包,并隱藏了 cgo 細節。這是另一個基本的嵌入式示例,這次使用 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 字符串。嵌入式看起來功能強大且對開發人員友好,是時候充分利用解釋器了:讓我們嘗試從磁盤加載 Python 模塊。

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

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

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

  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.  

構建時,我們需要將 PYTHONPATH 環境變量設置為當前工作目錄,以便導入語句能夠找到 foo.py 模塊。在 shell 中,該命令如下所示:

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

 

可怕的全局解釋器鎖

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

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

當一個常規的、非嵌入式的 Python 程序啟動時,不涉及 GIL 以避免鎖定操作中的無用開銷;在某些 Python 代碼首次請求生成線程時 GIL 就啟動了。對于每個線程,解釋器創建一個數據結構來存儲當前的相關狀態信息并鎖定 GIL。當線程完成時,狀態被恢復,GIL 被解鎖,準備被其他線程使用。

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

解決方案是在我們從 Go 運行多線程代碼時顯式調用 GIL;代碼并不復雜,因為 C API 提供了我們需要的所有工具。為了更好地暴露這個問題,我們需要寫一些受 CPU 限制的 Python 代碼。讓我們將這些函數添加到前面示例中的 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 并發打印奇數和偶數,使用兩個不同的 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.  

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

  1. 保存狀態并鎖定 GIL。
  2. 執行 Python。
  3. 恢復狀態并解鎖 GIL。

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

我們說過當從 Python 操作多線程時,解釋器負責創建存儲當前狀態所需的數據結構,但是當同樣的事情發生在 C API 時,我們來負責處理。

當我們用 go-python 初始化解釋器時,我們是在 Python 上下文中操作的。因此,當調用 PyEval_InitThreads() 時,它會初始化數據結構并鎖定 GIL。我們可以使用 PyEval_SaveThread() 和 PyEval_RestoreThread() 對已經存在的狀態進行操作。

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

 

釋放 Gopher

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

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

考慮到這一點,讓我們看看當一個 goroutine 被移動到一個新線程時,運行一些 Python 代碼的 goroutine 會發生什么:

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

所幸,我們可以通過從 goroutine 中調用運行時包中的 LockOSThread 函數來強制 Go runtime 始終保持我們的 goroutine 在同一線程上運行:

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

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

結論

為了嵌入 Python,Datadog Agent 必須接受一些權衡:

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

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

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

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

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

責任編輯:龐桂玉 來源: Linux中國
相關推薦

2023-06-15 13:01:07

JavaPythonJavaScript

2013-03-04 14:35:05

WordPressEdge AnimatHTML5

2015-07-06 09:59:56

JavaScript私有成員

2022-09-19 11:42:21

Go優化CPU

2023-11-26 19:06:13

GO測試

2010-08-10 15:55:20

FlexHTML頁面

2022-06-22 09:56:19

PythonMySQL數據庫

2021-07-02 07:18:19

Goresults通道類型

2021-11-08 10:58:08

變量依賴圖排序

2021-07-02 20:37:19

Python代碼SRP

2025-01-21 15:20:14

2020-09-15 10:45:06

PythonPyQt5Matplotlib

2023-09-01 08:19:21

Flask

2020-07-06 15:50:41

Python文件Linux

2018-11-05 14:53:14

Go函數代碼

2009-10-21 10:47:03

Siliverligh

2010-08-10 14:08:09

Flex嵌入字體

2009-02-17 23:51:57

Linux程序登錄界面

2024-11-14 10:00:00

Python繼承

2022-11-09 10:03:39

UbuntuLinux
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 淫片专区| 久久在线看 | 黄色在线免费看 | 黄网站在线观看 | 中文字幕爱爱视频 | 国产一区91精品张津瑜 | av在线黄 | 盗摄精品av一区二区三区 | 一区二区免费在线视频 | 国产麻豆乱码精品一区二区三区 | 久久精品福利视频 | 欧美一级在线观看 | 婷婷精品| 超碰91在线 | 久久久久网站 | 国产精品一区久久久 | 精品国产精品三级精品av网址 | 国产精品久久久久无码av | 亚洲视频免费在线观看 | 亚洲精品乱码久久久久久按摩观 | 91av免费看| 免费观看成人鲁鲁鲁鲁鲁视频 | 一区二区视频 | 久操伊人| 天天爱天天操 | 久久网站黄 | 中文字幕在线视频免费视频 | 亚洲福利网 | 毛片大全 | www.久草.com| 亚洲精品www | 午夜影院免费体验区 | 国产欧美综合在线 | 欧美一区二区免费 | 亚洲视频在线一区 | 国产精品亚洲视频 | 亚洲性在线 | 欧美天堂一区 | 久草电影网 | 精品久久香蕉国产线看观看亚洲 | 久久国产精品一区二区三区 |