嵌入 Julia
正如我們在 呼叫 C 和 Fortran 程式碼 中所看到的,Julia 有種簡單且有效率的方式來呼叫以 C 編寫的函式。但有些情況需要相反的方式:從 C 程式碼呼叫 Julia 函式。這可以用來將 Julia 程式碼整合到更大的 C/C++ 專案中,而不需要將所有內容都改寫成 C/C++。Julia 有個 C API 可以讓這件事成真。由於幾乎所有的程式語言都有呼叫 C 函式的方法,Julia C API 也可以用來建立進階的語言橋接(例如從 Python、Rust 或 C# 呼叫 Julia)。即使 Rust 和 C++ 可以直接使用 C 嵌入式 API,但兩者都有套件可以協助處理,對於 C++,Jluna 很有用。
高階嵌入
注意:本節說明在類 Unix 作業系統中將 Julia 程式碼嵌入 C。要在 Windows 上執行此操作,請參閱本節之後的 使用 Visual Studio 在 Windows 上進行高階嵌入。
我們從一個簡單的 C 程式開始,它會初始化 Julia 並呼叫一些 Julia 程式碼
#include <julia.h>
JULIA_DEFINE_FAST_TLS // only define this once, in an executable (not in a shared library) if you want fast code.
int main(int argc, char *argv[])
{
/* required: setup the Julia context */
jl_init();
/* run Julia commands */
jl_eval_string("print(sqrt(2.0))");
/* strongly recommended: notify Julia that the
program is about to terminate. this allows
Julia time to cleanup pending write requests
and run all finalizers
*/
jl_atexit_hook(0);
return 0;
}
為了建置這個程式,你必須將 Julia 標頭的的路徑新增到包含路徑中,並連結到 libjulia
。例如,當 Julia 安裝在 $JULIA_DIR
時,可以使用 gcc
編譯上述測試程式 test.c
gcc -o test -fPIC -I$JULIA_DIR/include/julia -L$JULIA_DIR/lib -Wl,-rpath,$JULIA_DIR/lib test.c -ljulia
或者,查看 Julia 原始碼樹中 test/embedding/
資料夾中的 embedding.c
程式。檔案 cli/loader_exe.c
程式是另一個簡單的範例,說明如何在連結到 libjulia
的同時設定 jl_options
選項。
在呼叫任何其他 Julia C 函式之前,必須先初始化 Julia。這是透過呼叫 jl_init
來完成的,它會嘗試自動判斷 Julia 的安裝位置。如果你需要指定自訂位置,或指定要載入哪個系統映像,請改用 jl_init_with_image
。
測試程式中的第二個陳述式使用對 jl_eval_string
的呼叫來評估 Julia 陳述式。
強烈建議在程式終止前呼叫 jl_atexit_hook
。上述範例程式在從 main
回傳前呼叫此函式。
目前,與 libjulia
共用函式庫進行動態連結需要傳遞 RTLD_GLOBAL
選項。在 Python 中,這看起來像
>>> julia=CDLL('./libjulia.dylib',RTLD_GLOBAL)
>>> julia.jl_init.argtypes = []
>>> julia.jl_init()
250593296
如果 julia 程式需要存取主執行檔中的符號,除了下面由 julia-config.jl
產生的旗標之外,可能需要在 Linux 上的編譯時間加入 -Wl,--export-dynamic
連結器旗標。在編譯共用函式庫時不需要這麼做。
使用 julia-config 自動決定建置參數
建立指令碼 julia-config.jl
是為了協助決定使用嵌入式 Julia 的程式需要哪些建置參數。此指令碼使用呼叫它的特定 Julia 發行版的建置參數和系統組態,來匯出嵌入式程式與該發行版互動所需的必要編譯器旗標。此指令碼位於 Julia 共用資料目錄中。
範例
#include <julia.h>
int main(int argc, char *argv[])
{
jl_init();
(void)jl_eval_string("println(sqrt(2.0))");
jl_atexit_hook(0);
return 0;
}
在命令列上
此腳本的簡單用法是從命令列。假設 julia-config.jl
位於 /usr/local/julia/share/julia
,它可以直接在命令列上呼叫,並採用三種旗標的任意組合
/usr/local/julia/share/julia/julia-config.jl
Usage: julia-config [--cflags|--ldflags|--ldlibs]
如果上述範例程式碼儲存在檔案 embed_example.c
中,則以下命令會將其編譯成 Linux 和 Windows (MSYS2 環境) 上的可執行程式。在 macOS 上,將 gcc
替換為 clang
。
/usr/local/julia/share/julia/julia-config.jl --cflags --ldflags --ldlibs | xargs gcc embed_example.c
使用於 Makefiles
一般來說,嵌入式專案會比上述範例複雜,因此以下也允許一般的 makefile 支援 – 由於使用 shell 巨集擴充,因此假設為 GNU make。此外,雖然 julia-config.jl
通常位於 /usr/local
目錄中,但如果沒有,則 Julia 本身可用於尋找 julia-config.jl
,而 makefile 可以利用此功能。上述範例已延伸為使用 makefile
JL_SHARE = $(shell julia -e 'print(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia"))')
CFLAGS += $(shell $(JL_SHARE)/julia-config.jl --cflags)
CXXFLAGS += $(shell $(JL_SHARE)/julia-config.jl --cflags)
LDFLAGS += $(shell $(JL_SHARE)/julia-config.jl --ldflags)
LDLIBS += $(shell $(JL_SHARE)/julia-config.jl --ldlibs)
all: embed_example
現在建置命令僅為 make
。
使用 Visual Studio 在 Windows 上進行高級嵌入
如果尚未設定 JULIA_DIR
環境變數,請在啟動 Visual Studio 之前使用「系統」面板新增它。JULIA_DIR 下的 bin
資料夾應位於系統路徑中。
我們從開啟 Visual Studio 和建立新的「主控台應用程式」專案開始。開啟「stdafx.h」標頭檔,並在結尾新增以下列
#include <julia.h>
然後,將專案中的 main() 函式替換為此程式碼
int main(int argc, char *argv[])
{
/* required: setup the Julia context */
jl_init();
/* run Julia commands */
jl_eval_string("print(sqrt(2.0))");
/* strongly recommended: notify Julia that the
program is about to terminate. this allows
Julia time to cleanup pending write requests
and run all finalizers
*/
jl_atexit_hook(0);
return 0;
}
下一步是設定專案以尋找 Julia 包含檔和函式庫。了解 Julia 安裝是 32 位元或 64 位元非常重要。在繼續之前,移除任何與 Julia 安裝不對應的平台組態。
使用專案的「屬性」對話方塊,前往 C/C++
| 一般
,並將 $(JULIA_DIR)\include\julia\
新增至「其他包含目錄」屬性。然後,前往 連結器
| 一般
區段,並將 $(JULIA_DIR)\lib
新增至「其他函式庫目錄」屬性。最後,在 連結器
| 輸入
下,將 libjulia.dll.a;libopenlibm.dll.a;
新增至函式庫清單。
此時,專案應可建置並執行。
轉換型別
實際應用程式不僅需要執行表達式,還需要將其值傳回給主機程式。jl_eval_string
會傳回 jl_value_t*
,這是指向堆疊配置 Julia 物件的指標。以這種方式儲存 Float64
等簡單資料型別稱為「封裝」,而擷取儲存的原始資料稱為「解封」。我們改良的範例程式會在 Julia 中計算 2 的平方根,並在 C 中讀回結果,其主體現在包含以下程式碼
jl_value_t *ret = jl_eval_string("sqrt(2.0)");
if (jl_typeis(ret, jl_float64_type)) {
double ret_unboxed = jl_unbox_float64(ret);
printf("sqrt(2.0) in C: %e \n", ret_unboxed);
}
else {
printf("ERROR: unexpected return type from sqrt(::Float64)\n");
}
為了檢查 ret
是否為特定 Julia 型別,我們可以使用 jl_isa
、jl_typeis
或 jl_is_...
函式。在 Julia shell 中輸入 typeof(sqrt(2.0))
,我們可以看到傳回型別為 Float64
(在 C 中為 double
)。若要將封裝的 Julia 值轉換為 C double,請在上述程式碼片段中使用 jl_unbox_float64
函式。
對應的 jl_box_...
函式用於進行反向轉換
jl_value_t *a = jl_box_float64(3.0);
jl_value_t *b = jl_box_float32(3.0f);
jl_value_t *c = jl_box_int32(3);
正如我們接下來將看到的,封裝是呼叫具有特定引數的 Julia 函式所必需的。
呼叫 Julia 函式
雖然 jl_eval_string
允許 C 取得 Julia 表達式的結果,但它不允許將在 C 中計算出的參數傳遞給 Julia。為此,您需要使用 jl_call
直接呼叫 Julia 函數
jl_function_t *func = jl_get_function(jl_base_module, "sqrt");
jl_value_t *argument = jl_box_float64(2.0);
jl_value_t *ret = jl_call1(func, argument);
在第一步中,透過呼叫 jl_get_function
來擷取 Julia 函數 sqrt
的句柄。傳遞給 jl_get_function
的第一個參數是指向定義 sqrt
的 Base
模組的指標。然後,使用 jl_box_float64
將雙值打包。最後,在最後一步中,使用 jl_call1
呼叫函數。jl_call0
、jl_call2
和 jl_call3
函數也存在,用於方便地處理不同數量的參數。若要傳遞更多參數,請使用 jl_call
jl_value_t *jl_call(jl_function_t *f, jl_value_t **args, int32_t nargs)
它的第二個參數 args
是 jl_value_t*
參數的陣列,而 nargs
是參數的數量。
還有一種替代且可能更簡單的呼叫 Julia 函數方法,那就是透過 @cfunction
。使用 @cfunction
允許您在 Julia 端執行類型轉換,這通常比在 C 端執行更容易。上述 sqrt
範例使用 @cfunction
會寫成
double (*sqrt_jl)(double) = jl_unbox_voidpointer(jl_eval_string("@cfunction(sqrt, Float64, (Float64,))"));
double ret = sqrt_jl(2.0);
在這裡,我們首先在 Julia 中定義一個 C 可呼叫函數,從中擷取函數指標,最後呼叫它。
記憶體管理
如我們所見,Julia 物件在 C 中表示為類型為 jl_value_t*
的指標。這引發了一個問題,即誰負責釋放這些物件。
通常,Julia 物件是由垃圾收集器 (GC) 釋放,但 GC 不會自動知道我們正在從 C 持有對 Julia 值的參照。這表示 GC 可以從您底下釋放物件,導致指標無效。
GC 僅會在配置新的 Julia 物件時執行。像 jl_box_float64
的呼叫會執行配置,但配置也可能發生在執行 Julia 程式碼的任何時間點。
在編寫嵌入 Julia 的程式碼時,通常可以在 jl_...
呼叫之間使用 jl_value_t*
值(因為 GC 僅會由這些呼叫觸發)。但為了確保值可以在 jl_...
呼叫中存活,我們必須告訴 Julia 我們仍持有 Julia 根 值的參考,這個程序稱為「GC 根化」。根化值將確保垃圾收集器不會意外地將此值識別為未使用,並釋放支援該值的記憶體。這可以使用 JL_GC_PUSH
巨集來完成
jl_value_t *ret = jl_eval_string("sqrt(2.0)");
JL_GC_PUSH1(&ret);
// Do something with ret
JL_GC_POP();
JL_GC_POP
呼叫會釋放由先前的 JL_GC_PUSH
建立的參考。請注意,JL_GC_PUSH
會將參考儲存在 C 堆疊中,因此在離開範圍之前,它必須與 JL_GC_POP
精確配對。也就是說,在函式傳回之前,或控制流程離開呼叫 JL_GC_PUSH
的區塊。
可以使用 JL_GC_PUSH2
到 JL_GC_PUSH6
巨集一次推入多個 Julia 值
JL_GC_PUSH2(&ret1, &ret2);
// ...
JL_GC_PUSH6(&ret1, &ret2, &ret3, &ret4, &ret5, &ret6);
若要推入 Julia 值陣列,可以使用 JL_GC_PUSHARGS
巨集,其使用方法如下
jl_value_t **args;
JL_GC_PUSHARGS(args, 2); // args can now hold 2 `jl_value_t*` objects
args[0] = some_value;
args[1] = some_other_value;
// Do something with args (e.g. call jl_... functions)
JL_GC_POP();
每個範圍只能呼叫 JL_GC_PUSH*
一次,且應僅與單一 JL_GC_POP
呼叫配對。如果您要根化的所有必要變數無法透過單一呼叫 JL_GC_PUSH*
推入,或者有超過 6 個變數要推入,且使用引數陣列不是一種選擇,那麼可以使用內部區塊
jl_value_t *ret1 = jl_eval_string("sqrt(2.0)");
JL_GC_PUSH1(&ret1);
jl_value_t *ret2 = 0;
{
jl_function_t *func = jl_get_function(jl_base_module, "exp");
ret2 = jl_call1(func, ret1);
JL_GC_PUSH1(&ret2);
// Do something with ret2.
JL_GC_POP(); // This pops ret2.
}
JL_GC_POP(); // This pops ret1.
請注意,在呼叫 JL_GC_PUSH*
之前,不必具有有效的 jl_value_t*
值。將它們初始化為 NULL
,傳遞給 JL_GC_PUSH*
,然後建立實際的 Julia 值是可以的。例如
jl_value_t *ret1 = NULL, *ret2 = NULL;
JL_GC_PUSH2(&ret1, &ret2);
ret1 = jl_eval_string("sqrt(2.0)");
ret2 = jl_eval_string("sqrt(3.0)");
// Use ret1 and ret2
JL_GC_POP();
如果需要在函數(或區塊範圍)之間保留變數指標,則無法使用 JL_GC_PUSH*
。在這種情況下,有必要建立並保留對 Julia 全域範圍內變數的參照。達成此目的的一種簡單方法是使用全域 IdDict
,它將保留參照,避免 GC 釋放配置。但是,此方法僅適用於可變類型。
// This functions shall be executed only once, during the initialization.
jl_value_t* refs = jl_eval_string("refs = IdDict()");
jl_function_t* setindex = jl_get_function(jl_base_module, "setindex!");
...
// `var` is the variable we want to protect between function calls.
jl_value_t* var = 0;
...
// `var` is a `Vector{Float64}`, which is mutable.
var = jl_eval_string("[sqrt(2.0); sqrt(4.0); sqrt(6.0)]");
// To protect `var`, add its reference to `refs`.
jl_call3(setindex, refs, var, var);
如果變數不可變,則需要將其包裝在等效的可變容器中,或最好在將其推送到 IdDict
之前包裝在 RefValue{Any}
中。在此方法中,必須使用 C 程式碼建立或填入容器,例如使用函數 jl_new_struct
。如果容器是由 jl_call*
建立的,則需要重新載入要在 C 程式碼中使用的指標。
// This functions shall be executed only once, during the initialization.
jl_value_t* refs = jl_eval_string("refs = IdDict()");
jl_function_t* setindex = jl_get_function(jl_base_module, "setindex!");
jl_datatype_t* reft = (jl_datatype_t*)jl_eval_string("Base.RefValue{Any}");
...
// `var` is the variable we want to protect between function calls.
jl_value_t* var = 0;
...
// `var` is a `Float64`, which is immutable.
var = jl_eval_string("sqrt(2.0)");
// Protect `var` until we add its reference to `refs`.
JL_GC_PUSH1(&var);
// Wrap `var` in `RefValue{Any}` and push to `refs` to protect it.
jl_value_t* rvar = jl_new_struct(reft, var);
JL_GC_POP();
jl_call3(setindex, refs, rvar, rvar);
GC 可以透過使用函數 delete!
從 refs
中移除對變數的參照來釋放變數的配置,前提是沒有其他任何地方保留對該變數的參照
jl_function_t* delete = jl_get_function(jl_base_module, "delete!");
jl_call2(delete, refs, rvar);
對於非常簡單的情況,可以另建一個 Vector{Any}
類型的全域容器,並在必要時從中擷取元素,甚至可以使用下列方式為每個指標建立一個全域變數
jl_module_t *mod = jl_main_module;
jl_sym_t *var = jl_symbol("var");
jl_binding_t *bp = jl_get_binding_wr(mod, var);
jl_checked_assignment(bp, mod, var, val);
更新 GC 管理物件的欄位
垃圾收集器也假設它知道每個指向較新世代物件的較舊世代物件。任何時候,如果指標更新會破壞此假設,都必須使用 jl_gc_wb
(寫入屏障)函數通知收集器,如下所示
jl_value_t *parent = some_old_value, *child = some_young_value;
((some_specific_type*)parent)->field = child;
jl_gc_wb(parent, child);
通常無法預測哪些值在執行階段會變舊,因此必須在所有明確儲存後插入寫入屏障。一個值得注意的例外是,如果 parent
物件才剛配置,而且自那之後沒有執行垃圾收集。請注意,大多數 jl_...
函數有時會呼叫垃圾收集。
當直接更新指標陣列的資料時,也需要寫入屏障。例如
jl_array_t *some_array = ...; // e.g. a Vector{Any}
void **data = (void**)jl_array_data(some_array);
jl_value_t *some_value = ...;
data[0] = some_value;
jl_gc_wb(some_array, some_value);
控制垃圾收集器
有一些函數可以控制 GC。在一般使用情況下,這些函數並非必要。
函數 | 說明 |
---|---|
jl_gc_collect() | 強制執行 GC |
jl_gc_enable(0) | 停用 GC,傳回先前的狀態為 int |
jl_gc_enable(1) | 啟用 GC,傳回先前的狀態為 int |
jl_gc_is_enabled() | 傳回目前的狀態為 int |
使用陣列
Julia 和 C 可以共用陣列資料而不需複製。以下範例將說明其運作方式。
Julia 陣列在 C 中由資料類型 jl_array_t*
表示。基本上,jl_array_t
是一個包含以下內容的結構:
- 資料類型的資訊
- 指向資料區塊的指標
- 陣列大小的資訊
為了簡化起見,我們從 1D 陣列開始。建立一個包含長度為 10 的 Float64 元素的陣列,可以這樣做
jl_value_t* array_type = jl_apply_array_type((jl_value_t*)jl_float64_type, 1);
jl_array_t* x = jl_alloc_array_1d(array_type, 10);
或者,如果您已經配置陣列,您可以產生一個薄封裝器來包覆其資料
double *existingArray = (double*)malloc(sizeof(double)*10);
jl_array_t *x = jl_ptr_to_array_1d(array_type, existingArray, 10, 0);
最後一個引數是一個布林值,表示 Julia 是否應取得資料的所有權。如果這個引數非零,當陣列不再被參照時,GC 會對資料指標呼叫 free
。
為了存取 x
的資料,我們可以使用 jl_array_data
double *xData = (double*)jl_array_data(x);
現在我們可以填入陣列
for(size_t i=0; i<jl_array_len(x); i++)
xData[i] = i;
現在讓我們呼叫一個對 x
執行就地運算的 Julia 函式
jl_function_t *func = jl_get_function(jl_base_module, "reverse!");
jl_call1(func, (jl_value_t*)x);
透過列印陣列,可以驗證 x
的元素現在已反轉。
存取傳回的陣列
如果一個 Julia 函式傳回一個陣列,jl_eval_string
和 jl_call
的傳回值可以轉型為 jl_array_t*
jl_function_t *func = jl_get_function(jl_base_module, "reverse");
jl_array_t *y = (jl_array_t*)jl_call1(func, (jl_value_t*)x);
現在可以使用 jl_array_data
存取 y
的內容,就像之前一樣。務必在使用陣列時保留對它的參照。
多維陣列
Julia 的多維陣列儲存在記憶體中的欄位優先順序。以下是建立一個 2D 陣列並存取其屬性的程式碼
// Create 2D array of float64 type
jl_value_t *array_type = jl_apply_array_type((jl_value_t*)jl_float64_type, 2);
jl_array_t *x = jl_alloc_array_2d(array_type, 10, 5);
// Get array pointer
double *p = (double*)jl_array_data(x);
// Get number of dimensions
int ndims = jl_array_ndims(x);
// Get the size of the i-th dim
size_t size0 = jl_array_dim(x,0);
size_t size1 = jl_array_dim(x,1);
// Fill array with data
for(size_t i=0; i<size1; i++)
for(size_t j=0; j<size0; j++)
p[j + size0*i] = i + j;
請注意,雖然 Julia 陣列使用從 1 開始的索引,但 C API 使用從 0 開始的索引(例如在呼叫 jl_array_dim
時),以便作為慣用的 C 程式碼來讀取。
例外
Julia 程式碼可能會擲出例外。例如,考慮
jl_eval_string("this_function_does_not_exist()");
這個呼叫看起來什麼都不做。然而,可以檢查是否擲出例外
if (jl_exception_occurred())
printf("%s \n", jl_typeof_str(jl_exception_occurred()));
如果您使用支援例外狀況的語言(例如 Python、C#、C++)從 Julia C API 呼叫,則將每個呼叫包裝到 libjulia
中,並使用檢查是否擲回例外狀況的函式,然後在主機語言中重新擲回例外狀況,是有意義的。
擲回 Julia 例外狀況
撰寫 Julia 可呼叫函式時,可能需要驗證引數並擲回例外狀況以指出錯誤。典型的類型檢查看起來像
if (!jl_typeis(val, jl_float64_type)) {
jl_type_error(function_name, (jl_value_t*)jl_float64_type, val);
}
可以使用函式擲回一般例外狀況
void jl_error(const char *str);
void jl_errorf(const char *fmt, ...);
jl_error
會取得 C 字串,而 jl_errorf
的呼叫方式類似於 printf
jl_errorf("argument x = %d is too large", x);
在此範例中,假設 x
為整數。
執行緒安全性
一般而言,Julia C API 並非完全執行緒安全。在多執行緒應用程式中嵌入 Julia 時,需要小心不要違反下列限制
jl_init()
在應用程式生命週期中只能呼叫一次。jl_atexit_hook()
也適用相同規則,且只能在jl_init()
之後呼叫。jl_...()
API 函式只能從呼叫jl_init()
的執行緒呼叫,或從 Julia 執行階段啟動的執行緒呼叫。不支援從使用者啟動的執行緒呼叫 Julia API 函式,且可能會導致未定義的行為和當機。
上述第二個條件表示您無法安全地從未由 Julia 啟動的執行緒呼叫 jl_...()
函式(呼叫 jl_init()
的執行緒為例外)。例如,下列內容不受支援,而且很可能會造成區段錯誤
void *func(void*)
{
// Wrong, jl_eval_string() called from thread that was not started by Julia
jl_eval_string("println(Threads.threadid())");
return NULL;
}
int main()
{
pthread_t t;
jl_init();
// Start a new thread
pthread_create(&t, NULL, func, NULL);
pthread_join(t, NULL);
jl_atexit_hook(0);
}
相反地,從同一個使用者建立的執行緒執行所有 Julia 呼叫會有效
void *func(void*)
{
// Okay, all jl_...() calls from the same thread,
// even though it is not the main application thread
jl_init();
jl_eval_string("println(Threads.threadid())");
jl_atexit_hook(0);
return NULL;
}
int main()
{
pthread_t t;
// Create a new thread, which runs func()
pthread_create(&t, NULL, func, NULL);
pthread_join(t, NULL);
}
從 Julia 本身啟動的執行緒呼叫 Julia C API 的範例
#include <julia/julia.h>
JULIA_DEFINE_FAST_TLS
double c_func(int i)
{
printf("[C %08x] i = %d\n", pthread_self(), i);
// Call the Julia sqrt() function to compute the square root of i, and return it
jl_function_t *sqrt = jl_get_function(jl_base_module, "sqrt");
jl_value_t* arg = jl_box_int32(i);
double ret = jl_unbox_float64(jl_call1(sqrt, arg));
return ret;
}
int main()
{
jl_init();
// Define a Julia function func() that calls our c_func() defined in C above
jl_eval_string("func(i) = ccall(:c_func, Float64, (Int32,), i)");
// Call func() multiple times, using multiple threads to do so
jl_eval_string("println(Threads.threadpoolsize())");
jl_eval_string("use(i) = println(\"[J $(Threads.threadid())] i = $(i) -> $(func(i))\")");
jl_eval_string("Threads.@threads for i in 1:5 use(i) end");
jl_atexit_hook(0);
}
如果我們使用 2 個 Julia 執行緒執行此程式碼,我們會取得下列輸出(注意:輸出會因執行和系統而異)
$ JULIA_NUM_THREADS=2 ./thread_example
2
[C 3bfd9c00] i = 1
[C 23938640] i = 4
[J 1] i = 1 -> 1.0
[C 3bfd9c00] i = 2
[J 1] i = 2 -> 1.4142135623730951
[C 3bfd9c00] i = 3
[J 2] i = 4 -> 2.0
[C 23938640] i = 5
[J 1] i = 3 -> 1.7320508075688772
[J 2] i = 5 -> 2.23606797749979
如您所見,Julia 執行緒 1 對應於 pthread ID 3bfd9c00,而 Julia 執行緒 2 對應於 ID 23938640,顯示在 C 層級確實使用了多個執行緒,而且我們可以安全地從這些執行緒呼叫 Julia C API 常式。