繁体   English   中英

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

[英]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);

开始

开始迭代... 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()) new Thread(DoWork).Start()Task.Run(()=>DoWork())并且我得到了完全相同的行为- 它现在Stop直到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 )正是这样做的,像getch这样的简单 I/O 操作也是如此。 但是也有一些 Win32 函数允许在等待时运行其他操作(例如WaitForSingleObjectExbAlertable设置为TRUE )。 在 STA 中,COM 和 .NET 使用最复杂的等待函数CoWaitForMultipleHandles ,它运行一个 COM 模式循环来处理调用 STA 的传入消息。 当您调用此函数或任何使用它的函数时,可以在满足等待条件或函数返回之前执行任意数量的传入 COM 调用和/或 APC 回调。 (另外:当您从一个线程向不同单元中的 COM 对象进行 COM 调用时也是如此——来自其他单元的调用者可以在调用线程的函数调用返回之前调用调用线程。)

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM