Oczekuj i SynchronizationContext w zarządzanym komponencie obsługiwanym przez niezarządzaną aplikację

[EDITED] To wydaje się być błędem we wdrażaniu RamAplikacja. Zdarzenia, o którym pisałemtutaj. Przywrócenie niewłaściwego kontekstu synchronizacji w wątku interfejsu użytkownika może poważnie wpłynąć na programistów komponentów takich jak ja. Celem nagrody jest zwrócenie większej uwagi na ten problem i nagrodzenie @MattSmith, którego odpowiedź pomogła go namierzyć.

Jestem odpowiedzialny za .NET WinFormsUserControlkomponent oparty na ActiveXstarsza niezarządzana aplikacja, przez interop COM. Wymaganiem czasu wykonywania jest .NET 4.0 + Microsoft.Bcl.Async.

Komponent zostanie utworzony i użyty w głównym wątku interfejsu STA aplikacji. Jego realizacja wykorzystujeasync/awaitoczekuje więc, że instancja serializującego kontekstu synchronizacji została zainstalowana w bieżącym wątku (tj.,WindowsFormsSynchronizationContext).

Zazwyczaj,WindowsFormsSynchronizationContext zostaje ustawiony przezApplication.Run, gdzie działa pętla wiadomości zarządzanej aplikacji. Naturalnie,nie dotyczy to niezarządzanej aplikacji hostai nie mam nad tym kontroli. Oczywiście aplikacja hosta nadal ma własną klasyczną pętlę komunikatów systemu Windows, więc serializowanie nie powinno być problememawait połączenia zwrotne kontynuacji.

Jednak żadne z dotychczasowych rozwiązań nie jest idealne, a nawet działa poprawnie. Oto sztuczny przykład, gdzieTest metoda jest wywoływana przez aplikację hosta:

Task testTask;

public void Test()
{
    this.testTask = TestAsync();
}

async Task TestAsync()
{
    Debug.Print("thread before await: {0}", Thread.CurrentThread.ManagedThreadId);

    var ctx1 = SynchronizationContext.Current;
    Debug.Print("ctx1: {0}", ctx1 != null? ctx1.GetType().Name: null);

    if (!(ctx1 is WindowsFormsSynchronizationContext))
        SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());

    var ctx2 = SynchronizationContext.Current;
    Debug.Print("ctx2: {0}", ctx2.GetType().Name);

    await TaskEx.Delay(1000);

    Debug.WriteLine("thread after await: {0}", Thread.CurrentThread.ManagedThreadId);

    var ctx3 = SynchronizationContext.Current;
    Debug.Print("ctx3: {0}", ctx3 != null? ctx3.GetType().Name: null);

    Debug.Print("ctx3 == ctx1: {0}, ctx3 == ctx2: {1}", ctx3 == ctx1, ctx3 == ctx2);
}

Wyjście debugowania:

thread before await: 1
ctx1: SynchronizationContext
ctx2: WindowsFormsSynchronizationContext
thread after await: 1
ctx3: SynchronizationContext
ctx3 == ctx1: True, ctx3 == ctx2: False

Chociaż kontynuuje w tym samym wątku,WindowsFormsSynchronizationContext kontekst instaluję wcześniej w bieżącym wątkuawait zostaje zresetowany do wartości domyślnejSynchronizationContext po tym z jakiegoś powodu.

Dlaczego się resetuje? Sprawdziłem, że mój komponent jest jedynym komponentem .NET używanym przez tę aplikację. Sama aplikacja dzwoniCoInitialize/OleInitialize prawidłowo.

Próbowałem teżkonfiguracjaWindowsFormsSynchronizationContext w konstruktorze statycznego obiektu singleton, więc jest instalowany w wątku po załadowaniu zarządzanego zestawu. To nie pomogło: kiedyTest jest później wywoływany w tym samym wątku, kontekst został już zresetowany do domyślnego.

Rozważam użycieniestandardowy kelner zaplanowaćawait oddzwonienia przezcontrol.BeginInvoke mojej kontroli, więc powyższe będzie wyglądałoawait TaskEx.Delay().WithContext(control). To powinno działać dla mnieawaits, o ile aplikacja hosta pompuje wiadomości, ale nie dlaawaits wewnątrz któregokolwiek z zespołów innych firm mój zestaw może się odwoływać.

Nadal to badam.Wszelkie pomysły dotyczące zachowania właściwego powinowactwa wątkuawait w tym scenariuszu byłoby mile widziane.

questionAnswers(2)

yourAnswerToTheQuestion