为什么我的 C# 代码在调用 C++ COM 直到 Task.Wait/Thread.Join 时停止?

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

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

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

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

在 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);

STARTING

Starting iteration... 0

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

Received: Message 0 reported

...Ending iteration 0

Starting iteration... 1

Received: Message 1 reported

...Ending iteration 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 似乎可以解除对呼叫的阻止,以及为什么您可能会在精简的用例中看到这个问题,而您却不这样做在更大的应用程序中查看它。

在 Windows 等待的是一只有趣的野兽。本能地,我们想象 Task.WaitThread.Join 将完全阻塞线程,直到满足等待条件。有 Win32 函数(例如 WaitForSingleObject) that do exactly that, and simple I/O operations like getch do as well. But there are also Win32 functions that allow other operations to run while waiting (e.g. WaitForSingleObjectEx with bAlertable set to TRUE). In an STA, COM and .NET use the most complex wait function CoWaitForMultipleHandles,其中 运行 是一个 COM 模式循环,用于处理调用 STA 的传入消息。当您调用此函数或使用它的任何函数时,任何数量的传入 COM 调用and/or APC 回调可以在满足等待条件或函数 returns 之前执行。(旁白:当您从一个线程对不同单元中的 COM 对象进行 COM 调用时也是如此 --来自其他单元的调用者可以在调用线程的函数调用之前调用调用线程 returns.)

至于为什么你没有在完整的应用程序中看到它,我猜你减少用例的简单性实际上给你带来了更多的痛苦。完整的应用程序可能有等待、消息泵、跨线程调用或其他一些最终允许 COM 调用在足够的时间通过的东西。如果完整的应用程序是 .NET,您应该知道与 COM 的互操作是 .NET 的基础,因此它不一定是您直接执行的操作可能会让 COM 调用通过。