在C#中,如何實現跟native dll 中途的線程間通信?

我在我的網站項目中,C#中使用一個自己寫的C++ dll處理大規模的運算,耗時很長。如果檢測到用戶取消計算,我想讓這個native線程退出。我試了一下傳入一個ref,但是在C#中改變數值的時候,C++裡面並不能接收到。

舉個簡化的例子:

C#:

[DllImport("Dll1.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void test(ref bool running, out UInt64 res);

static void Main(string[] args)
{
UInt64 res = 0;
bool running = true;
var task = Task.Factory.StartNew(() =&> test(ref running, out res));
if ((Console.ReadKey().KeyChar) != null)
running = false;
task.Wait();
Console.WriteLine(res);
Console.ReadKey();
}

C++:

extern "C" __declspec(dllexport) void test(volatile bool* running, unsigned long long* res)
{
default_random_engine rd;
uniform_int_distribution&<&> dist(0, 1);
while (*running *res &< numeric_limits&::max())
{
*res += dist(rd);
Sleep(100);
}
}

即使加了volatile, 實際上C++ dll仍然會無視C#部分對running的改變,耿直地跑下去.......

有沒有什麼辦法能把running = false 這個信息中途傳進C++ dll, 讓它結束?


volatile 用在多線程環境里基本所有用法都是錯誤的,何況這個過程還經過了 C# -&> C++ DLL 的處理。

簡單說,另開一個 DLLExport 的 API stop(),設置一個 C++ 里的全局 flag(用 atomic 或者加鎖),然後在這個 test 函數里 check 這個 flag 試試看。

更好的應該是把計算放在其它線程 / 進程,避免阻塞請求線程。


最安全的不會被優化干擾的辦法:把volatile bool* running改成bool(*running)()。然後C#傳遞lambda expression進去的時候記得把lambda expression本身也保存在一個變數里,否則會調用了一半中途被刪掉。


volatile按照語義,應當不會對線程產生有益幫助。你至少應當用atomic operation啊!

=========

我會選擇把控制能力都塞到C++代碼里去,留給C#的API全都是函數。

頭文件:

extern "C"
{
__declspec("dllexport") void stop_worker();
}

實現文件:

static int32_t should_stop;
extern "C"
{
__declspec("dllexport") void stop_worker()
{
InterlockedExchange(should_stop, 1);
}

// 在你的線程循環里,檢測should_stop

}


如果沒有跨語言調用的話,就沒有這個問題。這個問題的出現也不在鎖變數那兒,一個線程寫一個線程循環讀檢測並不會有什麼臨界的問題。

而是在於P/Invoke幫你做了很多你不知道的事情。

C#里的bool是託管了的,它就算是以ref的方式傳入C++,那也是從託管的數據中複製一份,放到一個臨時的C++的bool中,然後再取這個C++ bool的地址,作為指針傳入。

這點,你可以試著手動寫一下Python和c++互調用的包裝代碼,大概就知道了(逃。

解決方法就是C++ DLL中,定義一個變數,然後暴露一個函數給C#調用並修改那個變數就好了。


用 IntPtr 就沒問題了。

C#:

[DllImport("Dll1.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void test(IntPtr running, out UInt64 res);

static unsafe void Main(string[] args)
{
UInt64 res = 0;
bool running = true;
var p = new IntPtr(running);
var task = Task.Factory.StartNew(() =&> test(p, out res));
if ((Console.ReadKey().KeyChar) != null)
running = false;
task.Wait();
Console.WriteLine(res);
Console.ReadKey();
}

C++ 部分不變。


我自己回答一下,依靠各位大佬的幫助,我做出來一個work的:

C++ DLL:

#include &
#include &

using namespace std;

bool running = true;

extern "C" __declspec(dllexport) void test(unsigned* res)
{
default_random_engine rd;
uniform_int_distribution&<&> dist(0, 1);
while (running *res &< (numeric_limits&::max)())
{
*res += dist(rd);
Sleep(100);
}
}

extern "C" __declspec(dllexport) void stop()
{
running = false;
}

C# exe:

using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace ConsoleApp1
{
class Program
{
[DllImport("Dll1.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void test(out uint res);

[DllImport("Dll1.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void stop();

static void Main(string[] args)
{
uint res = 0;
var task = Task.Factory.StartNew(() =&> test(out res));
if ((Console.ReadKey().KeyChar) == "a")
stop();
task.Wait();
Console.WriteLine();
Console.WriteLine("The current sum is: {0}", res);
Console.ReadKey();
}
}
}

在C#運行的時候,按下a鍵就會切斷C++ DLL函數的運行,並得到當前的累加結果。可以看得到,切斷時間越短,累加結果越小,證明確實是及時切斷了運行。atomic在這個例子實際上是沒有必要的,因為C++函數本身並不寫入[running],但如果用進程池運行多個該函數,就有race condition了,要給每個Task ID分配一個獨立的空間來存儲running,否則一停就全部停止了。

在這個原型上,加上一點線程池管理和race condition管理,應該可以上網站實用。

我的解決方案是給每個任務安排一個任務ID,這樣就知道要切斷的是哪個任務。但是今天一晚上都在解決奇奇怪怪的bug,最後發現是unordered_map&的問題。我對它的寫入加了mutex,但是讀取,例如while (mapRunning[tid]) do_something(); 仍然會造成線程問題。目前暫時用了一個bool[10000]的數組解決,以後可以考慮用TBB::concurrent_unordered_map。


走消息,簡單易行。PostThreadMessage


c#的pinvoke實際上並不是直接的函數指針調用,所以你的ref不會持續起作用,CLR會在你調用這個帶ref參數的函數的時候臨時在棧上臨時分配個空間存數據,執行完畢後再更新到調用者提供的ref變數。所以執行完畢後這個ref就沒意義了。你實際上可以直接提供指針,就可以繞過ref的限制。


Volatile在C#和C++里是兩個意思,C++的volatile意思是就算標記的變數在代碼中從未被賦值也沒被改變也不要優化掉。

具體到你這裡,我建議把dll的test包在一個大類里,讓test 內容在固定線程上運行。增加一個void test_abort(),用來正確的退出線程。


在C++中提供一個方法,volatile應該是不work的

bool running = False;

bool WillRun(bool run)

{

lock;

running = run;

unlock;

}

extern "C" __declspec(dllexport) void test(unsigned long long* res)

{

default_random_engine rd;

uniform_int_distribution&<&> dist(0, 1);

lock;

run = running;

unlock;

while (run *res &< numeric_limits&::max())

{

*res += dist(rd);

Sleep(100);

}

}


一般這種需求都是用消息隊列實現的,用多線程是什麼鬼


推薦閱讀:

學C#需要學好C++么?
c++中子函數的局部變數在函數結束後是否會銷毀變數的內容?
c++中在局部空間用new運算符創建的變數是否會被銷毀?
c++怎麼檢測內存泄露,怎麼定位內存泄露?
為什麼我覺得 Objective-C 的內存管理比 C++ 要複雜得多?這類語言是否是趨勢?

TAG:NET | 並行編程 | C | C# |