为什么我的C#代码在调用回C++COM直到Task时会暂停.等待/线程.加入

Why does my C# code stall when calling back into C++ COM until Task.Wait/Thread.Join?

本文关键字:暂停 等待 加入 线程 Task 直到 代码 我的 调用 C++COM 为什么      更新时间:2023-10-16

我有一个本机C++应用程序调用C#模块,该模块应该运行自己的程序循环,并使用COM通过提供的回调对象将消息传递回C++。

跳到奇怪行为和问题的最后

这些C#方法是从C++通过COM:调用的

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

在C++中,我提供了ICallbackCCallback:的实现

#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;
};

我的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();

这是一个人为的例子,可以避免发布大量代码,但你可以看到,C#代码应该每秒循环一次,调用一个打印消息的C++方法。

如果我留下这行评论://callback.Message($"Message reported at {System.DateTime.Now}");的工作原理与人们想象的完全一样。如果我取消注释,那么会发生什么:

CCallback callback;
IInterface *pInterface = GetInterface();
cout << "Hit Enter to start" << endl;
getch();
hr = pInterface->Start(&callback);

启动

正在启动迭代。。。0

cout << "Hit Enter to stop" << endl;
getch();
pInterface->Stop();

收到:消息0报告

结束迭代0

正在启动迭代。。。1

收到:消息1报告

结束迭代1

(…等等。)

cout << "Hit Enter to exit" << endl;
getch();
return;

结论

因此,由于某种原因调用callback.Message正在暂停我的Task,直到调用Task.Wait。这究竟是为什么?它是如何被卡住的?等待任务是如何释放它的?我的假设是,通过COM的线程模型意味着我有某种死锁,但有人能更具体吗?

我个人认为在专用的Thread中运行这一切会更好,但这是现有应用程序的工作方式,所以我真的很好奇发生了什么。

更新

所以我测试了new Thread(DoWork).Start()Task.Run(()=>DoWork()),得到了完全相同的行为——它现在暂停,直到Stop调用Thread.Join

所以我认为COM出于某种原因暂停了整个CLR或类似的东西。

听起来像:

  1. 回调实现对象在STA单元线程(主线程)上实例化
  2. 该任务正在STA或MTA的独立线程上运行
  3. 来自后台线程的接口调用正在被封送回主线程
  4. 您的主线程中没有消息泵
  5. 调用task.Wait时,它运行一个循环,允许主线程处理COM调用

您可以通过检查以下内容来验证这一点:

  1. 您应该在C++客户端应用程序的主线程中显式调用CoInitializeEx。检查您在那里使用的线程模型。如果你没有调用它,请添加它。添加它能解决你的问题吗?我希望不会,但如果真的发生了,那就意味着COM和.NET之间的一些交互可能是故意的,但会让你绊倒
  2. 添加日志或设置调试器,以便查看哪些线程正在执行哪些代码。您的代码应该只在两个线程中运行——主线程和一个后台线程。当您重现问题条件时,我相信您会看到Message()方法实现是在主线程上调用的
  3. 用Windows应用程序替换控制台应用程序,或者只在控制台应用程序中运行消息泵。我相信你会看到绞刑没有发生

我还猜测了为什么Task.WaitThread.Join似乎取消了对调用的阻止,以及为什么当您在更大的应用程序中没有看到这个问题时,您可能会在精简的用例中看到这个问题。

在窗口等待是一个有趣的野兽。本能地,我们假设Task.WaitThread.Join将完全阻塞线程,直到满足等待条件。有一些Win32函数(例如WaitForSingleObject)可以做到这一点,而像getch这样的简单I/O操作也可以做到。但也有Win32函数允许其他操作在等待时运行(例如WaitForSingleObjectExbAlertable设置为TRUE)。在STA中,COM和.NET使用最复杂的等待函数CoWaitForMultipleHandles,该函数运行COM模式循环,处理调用STA的传入消息。当您调用此函数或任何使用它的函数时,在满足等待条件或函数返回之前,可以执行任何数量的传入COM调用和/或APC回调。(此外:当您从一个线程对另一个单元中的COM对象进行COM调用时也是如此——在调用线程的函数调用返回之前,来自其他单元的调用方可以调用到调用线程中。)

至于为什么你没有在完整的应用程序中看到它,我想你减少用例的简单性实际上会给你带来更多的痛苦。完整的应用程序可能有等待、消息泵、跨线程调用或其他一些最终允许COM调用在足够的时间内通过的东西。如果完整的应用程序是.NET,你应该知道与COM的互操作对.NET来说是非常重要的,所以你不一定直接做的事情会让COM调用通过。