C++ Library callback using C# in Unity and cross thread call
由社團文章討論所做的測試,主要是有人詢問,在外部 C++ library callback 函數中,呼叫 StartCoroutine 而導致錯誤訊息 StartCoroutine can only be called from the main thread。這明顯是一個跨執行緒的呼叫問題(Cross thread call)。
該錯誤是 Unity API 設計所導致的,Unity 禁止跨執行緒呼叫其 API。Unity API 設計並不是執行緒安全(Thread-safe),意思是當兩個 Thread 同一時間呼叫 Unity API 時,可能會因為 Race condition 而導致狀態或是程式錯誤。因此 Unity 設計禁止執行緒呼叫。
但討論中有種讓我以為這不是 multi-thread call 的問題,而是因為 library callback 就會發生該錯誤,但這跟我經驗中不太一樣。因此我做了一個測試專案,來驗證我的假設,只有當該 callback 是由另外一個 thread 呼叫時,才會發生該錯誤,若只是單純呼叫 callback,其錯誤不會發生。
C++ Library
首先需要先建立 C++ library,很幸運的在 [0] 找到很適合的範例,跟著一步一步做就可以了。專案的建立使用 Virtual Studio 2015 Community 的範例 DLL 空專案開始修改。根據所使用的程式庫再額外設定專案屬性。
規劃該 library 兩個 function calls,參數都傳入 callback function pointer,分別是 StdCallback 以及 ThreadCallback。StdCallback 直接呼叫傳入的 callback,而 ThreadCallback 則是先建立執行緒,在執行緒中再呼叫其 callback。
// Callback.h
#ifndef DLL_EXPOTER_H
#define DLL_EXPOTER_H
#ifdef WIN32
#define DLL_API __declspec(dllexport)
#else
#define DLL_API extern
#endif
#ifdef __cplusplus
extern "C" {
#endif
typedef void (__stdcall * ProgressCallback)(int);
DLL_API void StdCallback(ProgressCallback callback);
DLL_API void ThreadCallback(ProgressCallback callback);
#ifdef __cplusplus
}
#endif
#endif
StdCallback 實作相同容易:
// Callback.cpp
DLL_API void StdCallback(ProgressCallback callback)
{
callback(rand() % 1000);
}
至於 ThreadCallback 就顯得比較麻煩,如何在 C++ 建立執行緒是比較大的麻煩。想到之前使用過的 pthread,[1] 下載程式庫並且設定讓專案使用,建立執行緒的範例修改實做:
// Callback.cpp
void *doThread(void* args)
{
ProgressCallback callback = (ProgressCallback)args;
callback(rand() % 1000);
return NULL;
}
DLL_API void ThreadCallback(ProgressCallback callback)
{
pthread_t t;
pthread_create(&t, NULL, doThread, (void*)callback);
pthread_join(t, NULL);
}
最後編譯輸出 Callback.dll。
C# Test Code
要在 Unity C# 呼叫 Library,首先先把自訂函式庫 Callback.dll 以及參考到的函式庫 pthread.dll,一同複製到 Unity 專案資料夾 Assets/Plugins 中。
接著宣告 C# 連結 C++ 程式庫 call:
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate void ProgressCallback(int value);
[DllImport("Callback")]
static extern void StdCallback(ProgressCallback callback);
[DllImport("Callback")]
static extern void ThreadCallback(ProgressCallback callback);
撰寫測試程式,主要要測試在一般 callback 呼叫 Unity API 會不會發生問題,而 thread callback 則預期會重現錯誤 can only be called from the main thread:
void Start ()
{
Debug.LogFormat("Start, Main ThreadID: {0}", Thread.CurrentThread.ManagedThreadId);
StdCallback(this.OnStdCallback);
ThreadCallback(this.OnThreadCallback);
}
void OnStdCallback(int n)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
Debug.LogFormat("StdCallback: {0}, ThreadID: {1}", n, threadId);
this.StartCoroutine(this.EmptyCoroutine());
}
void OnThreadCallback(int n)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
Debug.LogFormat("ThreadCallback: {0}, ThreadID: {1}", n, threadId);
this.StartCoroutine(this.EmptyCoroutine());
}
IEnumerator EmptyCoroutine()
{
yield break;
}
結果於預期般,只有跨執行緒呼叫 Unity API 才會發生錯誤,若只是單純 callback 並不會(因為還是在 main thread 處理)。
完整專案可到 Github 查看。
Thread Synchronization
Unity C# 沒有像 .Net framework 的 ThreadWorker 或是 Control.InvokeRequired 那種東西可以使用,要從切換到 main thread 處理目前只有想到一種方法,Shared buffer。
就像是那 Race condition 經典的範例,producer–consumer 的模式,把 thread callback 把資料塞到 shared buffer,main thread update 再去檢查該 shared buffer 有沒有要處理的資料,然後清空 buffer。
List<int> sharedBuffer = new List<int>();
void OnThreadCallback(int n)
{
lock(this.sharedBuffer)
{
this.sharedBuffer.Add(n);
}
}
void Update()
{
lock(this.sharedBuffer)
{
foreach (var n in this.sharedBuffer)
{
// TODO: n
}
this.sharedBuffer.Clear();
}
}
沒有留言: