简体   繁体   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?

I have a native C++ application calling into a C# module, which is supposed to run it's own program loop and pass messages back to C++ through a supplied callback object, using COM.我有一个本地 C++ 应用程序调用 C# 模块,该模块应该运行它自己的程序循环,并使用 COM 通过提供的回调对象将消息传递回 C++。 I have an existing application to work from but mine has a weird bug.我有一个现有的应用程序可以使用,但我的有一个奇怪的错误。

Skip to the very end for the weird behaviour and question对于奇怪的行为和问题,跳到最后

These C# methods are called from C++ via COM:这些 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();
    }

In C++ I provide an implementation of ICallback , CCallback :在 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;
};

My C++ calling code is basically:我的 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();

This is a contrived example to avoid posting huge lumps of code but you can see the idea is the C# code is supposed to loop once a second, calling a C++ method which prints the message.这是一个人为的示例,以避免发布大量代码,但您可以看到这个想法是 C# 代码应该每秒循环一次,调用打印消息的 C++ 方法。

If I leave this line commented: //callback.Message($"Message reported at {System.DateTime.Now}");如果我留下这一行评论: //callback.Message($"Message reported at {System.DateTime.Now}"); it works exactly as one would imagine.它的工作原理与人们想象的完全一样。 If I uncomment it then what happens is:如果我取消注释,那么会发生什么:

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

STARTING开始

Starting iteration... 0开始迭代... 0

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

Received: Message 0 reported收到:报告消息 0

...Ending iteration 0 ...结束迭代 0

Starting iteration... 1开始迭代... 1

Received: Message 1 reported已收到:已报告消息 1

...Ending iteration 1 ...结束迭代 1

(... and so on.) (... 等等。)

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

Conclusion结论

So for some reason the call callback.Message is stalling my Task , until Task.Wait is called.因此,出于某种原因,调用callback.Message会拖延我的Task ,直到Task.Wait Why on earth would this be?这到底是为什么? How does it get stuck and how does waiting on the task release it?它是如何卡住的,等待任务如何释放它? My assumption is the threading model via COM means I have some sort of deadlock but can anyone be more specific?我的假设是通过 COM 的线程模型意味着我有某种死锁,但任何人都可以更具体吗?

I personally think running this all in a dedicated Thread is better but it's how an existing application works so I'm just really curious what is happening.我个人认为在专用Thread运行这一切更好,但这是现有应用程序的工作方式,所以我真的很好奇发生了什么。

UPDATE更新

So I tested new Thread(DoWork).Start() Vs Task.Run(()=>DoWork()) and I get the exact same behaviour - it stalls now until Stop calls Thread.Join .所以我测试了new Thread(DoWork).Start() Task.Run(()=>DoWork()) new Thread(DoWork).Start()Task.Run(()=>DoWork())并且我得到了完全相同的行为- 它现在Stop直到Stop调用Thread.Join

So I'm thinking COM for some reason is suspending the entire CLR or something along those lines.所以我认为 COM 出于某种原因正在暂停整个 CLR 或类似的东西。

It sounds like:这听起来像:

  1. Your Callback implementation object is instantiated on an STA apartment thread (the main thread).您的回调实现对象在 STA 单元线程(主线程)上实例化。
  2. The task is running on separate thread that is either STA or MTA.该任务在 STA 或 MTA 的单独线程上运行。
  3. The interface call from the background thread is being marshaled back to the main thread.来自后台线程的接口调用被封送回主线程。
  4. There is no message pump in your main thread.您的主线程中没有消息泵。
  5. When task.Wait is called, it runs a loop that allows COM calls to be processed by the main thread.task.Wait被调用时,它运行一个循环,允许主线程处理 COM 调用。

You can verify this by checking the following:您可以通过检查以下内容来验证这一点:

  1. You should be calling CoInitializeEx explicitly in your C++ client application's main thread.您应该在 C++ 客户端应用程序的主线程中显式调用CoInitializeEx Check the threading model you're using there.检查您在那里使用的线程模型。 If you're not calling that, add it.如果您不调用它,请添加它。 Does adding it fix your problem?添加它是否可以解决您的问题? I would expect not, but if it does, that means there's some interaction between COM and .NET that is probably by design but is tripping you up.我希望不会,但如果确实如此,那就意味着 COM 和 .NET 之间存在某种交互,这可能是设计使然,但会让您失望。
  2. Add logs or setup a debugger so that you can see which threads are executing what pieces of code.添加日志或设置调试器,以便您可以查看哪些线程正在执行哪些代码段。 Your code should be running in only two threads -- your main thread and one background thread.您的代码应该只在两个线程中运行——您的主线程和一个后台线程。 When you reproduce the problem condition, I believe you will see that the Message() method implementation is invoked on the main thread.当您重现问题情况时,我相信您会看到在主线程上调用了Message()方法实现。
  3. Replace your console application with a Windows application, or just run a message pump in your console application.将您的控制台应用程序替换为 Windows 应用程序,或者只是在您的控制台应用程序中运行一个消息泵。 I believe you will see that the hang does not occur.我相信你会看到挂起不会发生。

I also have a guess at why Task.Wait and Thread.Join seem to unblock the calls, and also why you may be seeing this issue on a trimmed-down use case when you don't see it in the larger application.我也有一个猜测,为什么Task.WaitThread.Join似乎解除了调用的阻塞,以及为什么当您在较大的应用程序中看不到它时,您可能会在精简的用例中看到这个问题。

Waiting in Windows is a funny beast.在 Windows 中等待是一个有趣的野兽。 Instinctively, we imagine that Task.Wait or Thread.Join will block the thread entirely until the wait condition is satisfied.本能地,我们想象Task.WaitThread.Join将完全阻塞线程,直到满足等待条件。 There are Win32 functions (eg WaitForSingleObject ) that do exactly that, and simple I/O operations like getch do as well. Win32 函数(例如WaitForSingleObject )正是这样做的,像getch这样的简单 I/O 操作也是如此。 But there are also Win32 functions that allow other operations to run while waiting (eg WaitForSingleObjectEx with bAlertable set to TRUE ).但是也有一些 Win32 函数允许在等待时运行其他操作(例如WaitForSingleObjectExbAlertable设置为TRUE )。 In an STA, COM and .NET use the most complex wait function CoWaitForMultipleHandles , which runs a COM modal loop that processes the incoming messages for the calling STA.在 STA 中,COM 和 .NET 使用最复杂的等待函数CoWaitForMultipleHandles ,它运行一个 COM 模式循环来处理调用 STA 的传入消息。 When you call this function or any function that uses it, any number of incoming COM calls and/or APC callbacks can execute before the wait condition is met or the function returns.当您调用此函数或任何使用它的函数时,可以在满足等待条件或函数返回之前执行任意数量的传入 COM 调用和/或 APC 回调。 (Aside: This is also true when you make a COM call from one thread to a COM object in a different apartment -- callers from other apartments can invoke into the calling thread before the calling thread's function call returns.) (另外:当您从一个线程向不同单元中的 COM 对象进行 COM 调用时也是如此——来自其他单元的调用者可以在调用线程的函数调用返回之前调用调用线程。)

As for why you're not seeing it in the full app, I would guess that the simplicity of your reduced use case is actually causing you more pain.至于为什么你没有在完整的应用程序中看到它,我猜你减少用例的简单性实际上给你带来了更多的痛苦。 The full app maybe has waits, message pumps, cross-thread calls or some other something that ends up allowing the COM calls through at sufficient times.完整的应用程序可能有等待、消息泵、跨线程调用或其他一些最终允许 COM 调用通过足够时间的东西。 If the full app is .NET, you should know that interoperation with COM is pretty fundamental to .NET, so it's not necessarily something you're doing directly that may be letting the COM calls through.如果完整的应用程序是 .NET,您应该知道与 COM 的互操作是 .NET 的基础,因此不一定是您直接执行的操作可能会导致 COM 调用通过。

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

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