Почему мой C# код останавливается при обратном вызове в C ++ COM до Task.Wait / Thread.Join? - PullRequest
2 голосов
/ 26 февраля 2020

У меня есть собственное приложение C ++, вызывающее модуль C#, который должен запускать свою собственную программу l oop и передавать сообщения обратно в C ++ через предоставленный объект обратного вызова, используя COM. У меня есть приложение для работы, но у меня странная ошибка.

Перейти к самому концу для странного поведения и вопрос

Эти C# методы вызывается из C ++ через COM:

[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# должен равняться l oop раз в секунду, вызывая метод 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);

STARTING

Запуск итерации ... 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 означает, что у меня какая-то тупик, но кто-то может быть более конкретным? c?

Лично я считаю, что запускать все это в выделенном Thread лучше, но это как существующий приложение работает, поэтому мне просто очень интересно, что происходит.

ОБНОВЛЕНИЕ

Итак, я проверил new Thread(DoWork).Start() Vs Task.Run(()=>DoWork()), и я получил точно такое же поведение - теперь он останавливается до тех пор, пока Stop не вызовет Thread.Join.

Так что я думаю, что COM по какой-то причине приостанавливает весь CLR или что-то в этом роде.

1 Ответ

2 голосов
/ 26 февраля 2020

Звучит так:

  1. Ваш объект реализации Callback создается в потоке квартиры STA (основной поток).
  2. Задача выполняется в отдельном потоке, который является либо STA или MTA.
  3. Интерфейсный вызов из фонового потока перенаправляется обратно в основной поток.
  4. В вашем основном потоке сообщений нет.
  5. Когда task.Wait вызывается, запускается al oop, что позволяет обрабатывать COM-вызовы основным потоком.

Это можно проверить, проверив следующее:

  1. You должен явно вызывать CoInitializeEx в основном потоке вашего клиентского приложения на C ++. Проверьте модель потоков, которую вы там используете. Если вы этого не называете, добавьте это. Добавит ли это решение вашей проблемы? Я бы не ожидал, но если это произойдет, это означает, что есть некоторое взаимодействие между COM и. NET, которое, вероятно, разработано, но сбивает вас с толку.
  2. Добавьте журналы или настройте отладчик, чтобы вы могли видеть какие потоки выполняют какие части кода. Ваш код должен быть запущен только в двух потоках - основной поток и один фоновый поток. Когда вы воспроизводите условие проблемы, я полагаю, вы увидите, что реализация метода Message() вызывается в главном потоке.
  3. Замените консольное приложение на приложение Windows или просто запустите насос сообщений в ваше консольное приложение. Я полагаю, вы увидите, что зависание не происходит.

У меня также есть предположение, почему Task.Wait и Thread.Join, кажется, разблокируют вызовы, а также почему вы можете видеть эту проблему в урезанном случае, когда вы не видите его в более крупном приложении.

Ожидание Windows - забавный зверь. Инстинктивно мы представляем, что Task.Wait или Thread.Join будут полностью блокировать поток, пока не будет выполнено условие ожидания. Есть функции Win32 (например, WaitForSingleObject), которые делают именно это, и простые операции ввода-вывода, такие как getch, также делают. Но есть также функции Win32, которые позволяют другим операциям запускаться во время ожидания (например, WaitForSingleObjectEx с bAlertable, установленным на TRUE). В STA, COM и. NET используется наиболее сложная функция ожидания CoWaitForMultipleHandles, которая запускает модальный COM l oop, который обрабатывает входящие сообщения для вызывающей STA. Когда вы вызываете эту функцию или любую функцию, которая ее использует, любое количество входящих COM-вызовов и / или обратных вызовов AP C может выполняться до того, как будет выполнено условие ожидания или функция вернется. (Кроме того: это также верно, когда вы делаете вызов COM из одного потока в объект COM в другой квартире - вызывающие абоненты из других квартир могут вызывать в вызывающем потоке до возврата вызова функции вызывающего потока.)

Что касается того, почему вы не видите его в полном приложении, я бы предположил, что простота вашего сокращенного варианта использования на самом деле причиняет вам больше боли. В полном приложении может быть ожидание, рассылка сообщений, межпотоковые вызовы или что-то еще, что в итоге позволяет COM-вызовам проходить через достаточное время. Если полное приложение. NET, вы должны знать, что взаимодействие с COM довольно фундаментально для. NET, поэтому не обязательно то, что вы делаете напрямую, может пропускать вызовы COM.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...