首页 > 解决方案 > 为什么我的 C# 代码在回调到 C++ COM 时会停止,直到 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++ 中,我提供了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;
};

我的 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()VsTask.Run(()=>DoWork())并且我得到了完全相同的行为- 它现在停止直到Stop调用Thread.Join

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

标签: c#c++comtask-parallel-library

解决方案


这听起来像:

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

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

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

我也猜想为什么Task.Wait并且Thread.Join似乎解除了对呼叫的阻止,以及为什么当您在较大的应用程序中看不到此问题时,您可能会在精简的用例中看到此问题。

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

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


推荐阅读