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 WinFormsUserControl
komponent 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/await
oczekuje 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.