为啥我的 C# 代码在回调到 C++ COM 时会停止,直到 Task.Wait/Thread.Join?

Posted

技术标签:

【中文标题】为啥我的 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 时会停止,直到 Task.Wait/Thread.Join? 【发布时间】:2020-02-26 10:19:48 【问题描述】:

我有一个本地 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);

开始

开始迭代... 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(()=&gt;DoWork()),我得到了完全相同的行为 - 它现在停滞不前,直到Stop 调用Thread.Join

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

【问题讨论】:

我真的不知道这是否是 COM 问题 COM接口的线程模型是什么? @RamblinRose 鉴于接口是在 C# 中定义的,在哪里或如何描述?我已经从现有项目中复制了这个,所以我最好的猜测是我错过了一些代码或项目设置,但我很难找到任何东西 【参考方案1】:

听起来像:

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

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

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

我也猜想为什么Task.WaitThread.Join 似乎解除了对呼叫的阻止,以及为什么您可能会在一个精简的用例中看到这个问题,而您在更大的应用程序中看不到它.

在 Windows 中等待是一个有趣的野兽。本能地,我们想象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 调用通过。

【讨论】:

谢谢迈克尔,这些都是我认为应该受到指责的事情,但我的难题是我从(已部署的)应用程序中复制粘贴,该应用程序似乎使用CoInitializeEx 或 Windows 消息循环。我偷偷怀疑这可能隐藏在我没有注意到的一些库代码中。 据我了解,我的 C# COM 对象无论如何都存在于一个单独的线程中,用于从本机 C++ 进行 CLR 编组/安全,因此您的消息循环观察听起来可能对我来说。我会明确测试 @Mr.Boy 我已经用一些关于等待的信息更新了我的答案,这可能有助于您进一步了解问题。您首先应该弄清楚的最重要的信息是哪个线程正在处理Message() 调用。如果它是主线程,我上面所说的一切都适用。 调试在使 C# DLL 可调试方面被证明是一个问题。这意味着我在测试时会在更大程度上盲目飞行;)无论如何,我会尝试一些这些东西...... 好的,所以我需要进行更多测试以确定最佳解决方案,但我可以确认用CoInitializeEx(COINIT_MULTITHREADED) 替换CoInitialize 解决了问题(COINIT_APARTMENTTHREADED 没有帮助)。非常感谢¬

以上是关于为啥我的 C# 代码在回调到 C++ COM 时会停止,直到 Task.Wait/Thread.Join?的主要内容,如果未能解决你的问题,请参考以下文章

从 C++ COM DLL 回调到 C# 应用程序

C++ 回调函数到 C#

为啥我的 C# 和 C++ dll 表现出不同的行为?

为啥我的 C# 数组在转换为对象时会丢失类型符号信息?

C++ 到 C# 回调结构编组

C++:SDL 音频回调最终停止工作