簡體   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