W jaki sposób asynchronizacja C # 5 pomoże w problemach z synchronizacją wątków interfejsu użytkownika?

16

Słyszałem gdzieś, że asynchroniczne oczekiwanie na C # 5 będzie tak niesamowite, że nie będziesz musiał się tym martwić:

if (InvokeRequired)
{
    BeginInvoke(...);
    return;
}
// do your stuff here

Wygląda na to, że wywołanie zwrotne operacji oczekującej nastąpi w oryginalnym wątku osoby dzwoniącej. Eric Lippert i Anders Hejlsberg kilkakrotnie stwierdzili, że ta funkcja powstała w wyniku potrzeby lepszego reagowania interfejsów użytkownika (szczególnie interfejsów urządzeń dotykowych).

Myślę, że częstym użyciem takiej funkcji byłoby coś takiego:

public class Form1 : Form
{
    // ...
    async void GetFurtherInfo()
    {
        var temperature = await GetCurrentTemperatureAsync();
        label1.Text = temperature;
    }
}

Jeśli zostanie użyte tylko wywołanie zwrotne, ustawienie tekstu etykiety spowoduje wyjątek, ponieważ nie jest ono wykonywane w wątku interfejsu użytkownika.

Jak dotąd nie mogłem znaleźć żadnych zasobów potwierdzających, że tak jest. Czy ktoś o tym wie? Czy są jakieś dokumenty wyjaśniające technicznie, jak to będzie działać?

Podaj link z wiarygodnego źródła, nie odpowiadaj tylko „tak”.

Alex
źródło
Wydaje się to bardzo mało prawdopodobne, przynajmniej w zakresie awaitfunkcjonalności. To tylko dużo cukru syntaktycznego do kontynuacji . Być może istnieją inne niepowiązane ulepszenia WinForm, które powinny pomóc? To byłoby objęte samą platformą .NET, a nie C #.
Aaronaught
@Aaronaught Zgadzam się, dlatego właśnie zadaję to pytanie. Zredagowałem pytanie, aby wyjaśnić, skąd pochodzę. Brzmi dziwnie, że stworzyliby tę funkcję i nadal wymagają użycia niesławnego stylu kodu InvokeRequired.
Alex

Odpowiedzi:

17

Myślę, że tutaj wprowadzasz kilka rzeczy w błąd. Co pytasz, jest już możliwe przy użyciu System.Threading.TasksThe asynca awaitw C # 5 są właśnie zamierza dostarczyć trochę ładniejszy cukier syntaktyczny dla tej samej funkcji.

Użyjmy przykładu Winforms - upuść przycisk i pole tekstowe na formularzu i użyj tego kodu:

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
        .ContinueWith(t => DelayedAdd(t.Result, 20))
        .ContinueWith(t => DelayedAdd(t.Result, 30))
        .ContinueWith(t => DelayedAdd(t.Result, 50))
        .ContinueWith(t => textBox1.Text = t.Result.ToString(),
            TaskScheduler.FromCurrentSynchronizationContext());
}

private int DelayedAdd(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Uruchom go, a zobaczysz, że (a) nie blokuje wątku interfejsu użytkownika i (b) nie pojawia się zwykły błąd „niepoprawna operacja wielowątkowa” - chyba że usuniesz TaskSchedulerargument z ostatniego ContinueWith, w w którym przypadku będziesz.

Jest to standardowy sposób przekazywania kontynuacji . Magia dzieje się w TaskSchedulerklasie, a konkretnie w instancji, którą odzyskuje FromCurrentSynchronizationContext. Przekaż to do dowolnej kontynuacji, a powiesz, że kontynuacja musi działać na dowolnym wątku o nazwie FromCurrentSynchronizationContextmetoda - w tym przypadku na wątku interfejsu użytkownika.

Oczekujący są nieco bardziej wyrafinowani w tym sensie, że są świadomi, w którym wątku rozpoczęli i w którym wątku musi nastąpić kontynuacja. Tak więc powyższy kod można napisać nieco bardziej naturalnie:

private async void button1_Click(object sender, EventArgs e)
{
    int a = await DelayedAddAsync(5, 10);
    int b = await DelayedAddAsync(a, 20);
    int c = await DelayedAddAsync(b, 30);
    int d = await DelayedAddAsync(c, 50);
    textBox1.Text = d.ToString();
}

private async Task<int> DelayedAddAsync(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Te dwa powinny wyglądać bardzo podobnie, a w rzeczywistości bardzo podobne. DelayedAddAsyncMetoda teraz zwraca Task<int>zamiast int, a więc awaitjest tylko bicie kontynuacje na każdym z nich. Główną różnicą jest to, że przechodzi przez kontekst synchronizacji w każdej linii, więc nie musisz robić tego wyraźnie, jak to zrobiliśmy w poprzednim przykładzie.

Teoretycznie różnice są znacznie bardziej znaczące. W drugim przykładzie każda pojedyncza linia w button1_Clickmetodzie jest faktycznie wykonywana w wątku interfejsu użytkownika, ale samo zadanie ( DelayedAddAsync) działa w tle. W pierwszym przykładzie wszystko działa w tle , z wyjątkiem przypisania, do textBox1.Textktórego wyraźnie przypisaliśmy kontekst synchronizacji wątku interfejsu użytkownika.

To jest naprawdę interesujące await- fakt, że oczekujący jest w stanie wskoczyć i wyjść z tej samej metody bez blokowania wywołań. Dzwonisz await, bieżący wątek wraca do przetwarzania wiadomości, a kiedy to się skończy, oczekujący wybierze dokładnie tam, gdzie go przerwał, w tym samym wątku, w którym przerwał. Ale jeśli chodzi o twój Invoke/ BeginInvokekontrast w pytaniu, ja ' Przykro mi to mówić, że powinieneś przestać to robić dawno temu.

Aaronaught
źródło
To bardzo interesujące @Aaronaught. Byłem świadomy stylu przekazywania kontynuacji, ale nie byłem świadomy tego całego „kontekstu synchronizacji”. Czy istnieje dokument łączący ten kontekst synchronizacji z asynchronicznym oczekiwaniem w C # 5? Rozumiem, że jest to istniejąca funkcja, ale fakt, że używają jej domyślnie, brzmi jak wielka sprawa, szczególnie dlatego, że musi mieć duży wpływ na wydajność, prawda? Jakieś dalsze uwagi na ten temat? Nawiasem mówiąc, dziękuję za odpowiedź.
Alex
1
@Alex: Aby uzyskać odpowiedzi na wszystkie pytania uzupełniające, sugeruję przeczytanie Async Performance: Understanding the Costs of Async and Await . Sekcja „Dbaj o kontekst” wyjaśnia, w jaki sposób wszystko to odnosi się do kontekstu synchronizacji.
Aaronaught
(Nawiasem mówiąc, konteksty synchronizacji nie są nowe; są w frameworku od 2.0. TPL sprawiło, że są one znacznie łatwiejsze w użyciu.)
Aaronaught 16.10.11
2
Zastanawiam się, dlaczego wciąż jest wiele dyskusji na temat używania stylu InvokeRequired, a większość wątków, które widziałem, nawet nie wspominają o kontekstach synchronizacji. Zaoszczędziłbym czas na postawienie tego pytania ...
Alex
2
@Alex: Chyba nie wyglądałeś we właściwych miejscach . Nie wiem co ci powiedzieć; istnieje duża część społeczności .NET, której nadrobienie zajmuje dużo czasu. Do diabła, wciąż widzę niektórych programistów używających ArrayListklasy w nowym kodzie. Sam wciąż nie mam prawie żadnego doświadczenia z RX. Ludzie dowiadują się, co powinni wiedzieć, i dzielą się tym, co już wiedzą, nawet jeśli to, co już wiedzą, jest nieaktualne. Ta odpowiedź może być nieaktualna za kilka lat.
Aaronaught 16.10.11
4

Tak, w przypadku wątku interfejsu użytkownika wywołanie zwrotne operacji oczekującej nastąpi w oryginalnym wątku osoby wywołującej.

Eric Lippert napisał o tym 8-częściową serię rok temu: Fabulous Adventures In Coding

EDYCJA: a tutaj // kompilacja / prezentacja Andersa: Andersa channel9

BTW, czy zauważyłeś, że jeśli wywrócisz „// build /” do góry nogami, otrzymasz „/ plinq //” ;-)

Nicholas Butler
źródło