關於 web service, unity, blogger 等軟體工程筆記

C++ Library callback using C# in Unity and cross thread call

Edit icon 沒有留言
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 查看。

View on 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();
}
}

Reference

沒有留言: