[英]Why does my C# code stall when calling back into C++ COM until Task.Wait/Thread.Join?
I have a native C++ application calling into a C# module, which is supposed to run it's own program loop and pass messages back to C++ through a supplied callback object, using COM.我有一个本地 C++ 应用程序调用 C# 模块,该模块应该运行它自己的程序循环,并使用 COM 通过提供的回调对象将消息传递回 C++。 I have an existing application to work from but mine has a weird bug.
我有一个现有的应用程序可以使用,但我的有一个奇怪的错误。
Skip to the very end for the weird behaviour and question对于奇怪的行为和问题,跳到最后
These C# methods are called from C++ via COM:这些 C# 方法是通过 COM 从 C++ 调用的:
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("...")]
public interface IInterface
{
void Start(ICallback callback);
void Stop();
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("...")]
public interface ICallback
{
void Message(string message);
}
[Guid("...")]
public class MyInterface : IInterface
{
private Task task;
private CancellationTokenSource cancellation;
ICallback callback;
public void Start(ICallback callback)
{
Console.WriteLine("STARTING");
this.callback = callback;
this.cancellation = new CancellationTokenSource();
this.task = Task.Run(() => DoWork(), cancellation.Token);
Console.WriteLine("Service STARTED");
}
private void DoWork()
{
int i = 0;
while (!cancellation.IsCancellationRequested)
{
Task.Delay(1000, cancellation.Token).Wait();
Console.WriteLine("Starting iteration... {0}", i);
//callback.Message($"Message {0} reported");
Console.WriteLine("...Ending iteration {0}", i++);
}
Console.WriteLine("Service CANCELLED");
cancellation.Token.ThrowIfCancellationRequested();
}
public void Stop()
{
//cancellation.Cancel(); -- commented deliberately for testing
task.Wait();
}
In C++ I provide an implementation of ICallback
, CCallback
:在 C++ 中,我提供了
ICallback
、 CCallback
的实现:
#import "Interfaces.tlb" named_guids
class CCallback : public ICallback
{
public:
//! \brief Constructor
CCallback()
: m_nRef(0) { }
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject);
virtual HRESULT __stdcall raw_Message(BSTR message)
{
std::wstringstream ss;
ss << "Received: " << message;
wcout << ss.str() << endl;
return S_OK;
}
private:
long m_nRef;
};
My C++ calling code is basically:我的 C++ 调用代码基本上是:
CCallback callback;
IInterface *pInterface = GetInterface();
cout << "Hit Enter to start" << endl;
getch();
hr = pInterface->Start(&callback);
cout << "Hit Enter to stop" << endl;
getch();
pInterface->Stop();
cout << "Hit Enter to exit" << endl;
getch();
pInterface->Stop();
This is a contrived example to avoid posting huge lumps of code but you can see the idea is the C# code is supposed to loop once a second, calling a C++ method which prints the message.这是一个人为的示例,以避免发布大量代码,但您可以看到这个想法是 C# 代码应该每秒循环一次,调用打印消息的 C++ 方法。
If I leave this line commented: //callback.Message($"Message reported at {System.DateTime.Now}");
如果我留下这一行评论:
//callback.Message($"Message reported at {System.DateTime.Now}");
it works exactly as one would imagine.它的工作原理与人们想象的完全一样。 If I uncomment it then what happens is:
如果我取消注释,那么会发生什么:
CCallback callback;
IInterface *pInterface = GetInterface();
cout << "Hit Enter to start" << endl;
getch();
hr = pInterface->Start(&callback);
STARTING
开始
Starting iteration... 0
开始迭代... 0
cout << "Hit Enter to stop" << endl;
getch();
pInterface->Stop();
Received: Message 0 reported
收到:报告消息 0
...Ending iteration 0
...结束迭代 0
Starting iteration... 1
开始迭代... 1
Received: Message 1 reported
已收到:已报告消息 1
...Ending iteration 1
...结束迭代 1
(... and so on.) (... 等等。)
cout << "Hit Enter to exit" << endl;
getch();
return;
So for some reason the call callback.Message
is stalling my Task
, until Task.Wait
is called.因此,出于某种原因,调用
callback.Message
会拖延我的Task
,直到Task.Wait
。 Why on earth would this be?这到底是为什么? How does it get stuck and how does waiting on the task release it?
它是如何卡住的,等待任务如何释放它? My assumption is the threading model via COM means I have some sort of deadlock but can anyone be more specific?
我的假设是通过 COM 的线程模型意味着我有某种死锁,但任何人都可以更具体吗?
I personally think running this all in a dedicated Thread
is better but it's how an existing application works so I'm just really curious what is happening.我个人认为在专用
Thread
运行这一切更好,但这是现有应用程序的工作方式,所以我真的很好奇发生了什么。
So I tested new Thread(DoWork).Start()
Vs Task.Run(()=>DoWork())
and I get the exact same behaviour - it stalls now until Stop
calls Thread.Join
.所以我测试了
new Thread(DoWork).Start()
Task.Run(()=>DoWork())
new Thread(DoWork).Start()
与Task.Run(()=>DoWork())
并且我得到了完全相同的行为- 它现在Stop
直到Stop
调用Thread.Join
。
So I'm thinking COM for some reason is suspending the entire CLR or something along those lines.所以我认为 COM 出于某种原因正在暂停整个 CLR 或类似的东西。
It sounds like:这听起来像:
task.Wait
is called, it runs a loop that allows COM calls to be processed by the main thread.task.Wait
被调用时,它运行一个循环,允许主线程处理 COM 调用。 You can verify this by checking the following:您可以通过检查以下内容来验证这一点:
CoInitializeEx
explicitly in your C++ client application's main thread.CoInitializeEx
。 Check the threading model you're using there.Message()
method implementation is invoked on the main thread.Message()
方法实现。 I also have a guess at why Task.Wait
and Thread.Join
seem to unblock the calls, and also why you may be seeing this issue on a trimmed-down use case when you don't see it in the larger application.我也有一个猜测,为什么
Task.Wait
和Thread.Join
似乎解除了调用的阻塞,以及为什么当您在较大的应用程序中看不到它时,您可能会在精简的用例中看到这个问题。
Waiting in Windows is a funny beast.在 Windows 中等待是一个有趣的野兽。 Instinctively, we imagine that
Task.Wait
or Thread.Join
will block the thread entirely until the wait condition is satisfied.本能地,我们想象
Task.Wait
或Thread.Join
将完全阻塞线程,直到满足等待条件。 There are Win32 functions (eg WaitForSingleObject
) that do exactly that, and simple I/O operations like getch
do as well. Win32 函数(例如
WaitForSingleObject
)正是这样做的,像getch
这样的简单 I/O 操作也是如此。 But there are also Win32 functions that allow other operations to run while waiting (eg WaitForSingleObjectEx
with bAlertable
set to TRUE
).但是也有一些 Win32 函数允许在等待时运行其他操作(例如
WaitForSingleObjectEx
与bAlertable
设置为TRUE
)。 In an STA, COM and .NET use the most complex wait function CoWaitForMultipleHandles
, which runs a COM modal loop that processes the incoming messages for the calling STA.在 STA 中,COM 和 .NET 使用最复杂的等待函数
CoWaitForMultipleHandles
,它运行一个 COM 模式循环来处理调用 STA 的传入消息。 When you call this function or any function that uses it, any number of incoming COM calls and/or APC callbacks can execute before the wait condition is met or the function returns.当您调用此函数或任何使用它的函数时,可以在满足等待条件或函数返回之前执行任意数量的传入 COM 调用和/或 APC 回调。 (Aside: This is also true when you make a COM call from one thread to a COM object in a different apartment -- callers from other apartments can invoke into the calling thread before the calling thread's function call returns.)
(另外:当您从一个线程向不同单元中的 COM 对象进行 COM 调用时也是如此——来自其他单元的调用者可以在调用线程的函数调用返回之前调用调用线程。)
As for why you're not seeing it in the full app, I would guess that the simplicity of your reduced use case is actually causing you more pain.至于为什么你没有在完整的应用程序中看到它,我猜你减少用例的简单性实际上给你带来了更多的痛苦。 The full app maybe has waits, message pumps, cross-thread calls or some other something that ends up allowing the COM calls through at sufficient times.
完整的应用程序可能有等待、消息泵、跨线程调用或其他一些最终允许 COM 调用通过足够时间的东西。 If the full app is .NET, you should know that interoperation with COM is pretty fundamental to .NET, so it's not necessarily something you're doing directly that may be letting the COM calls through.
如果完整的应用程序是 .NET,您应该知道与 COM 的互操作是 .NET 的基础,因此不一定是您直接执行的操作可能会导致 COM 调用通过。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.