2015-01-29 13 views
8

Podążałem za this question i rozumiem powody tej popularnej (aczkolwiek nieuznanej) odpowiedzi przez Peter Duniho. W szczególności, to sobie sprawę, że nie oczekujących na kolejne długotrwała praca blokuje nić ui:Czy należy zagnieżdżać oczekiwane operacje?

Drugi przykład nie daje w asynchronicznej. Zamiast tego, pobierając wartość właściwości content.Result, zmuszasz bieżący wątek do oczekiwania na zakończenie operacji asynchronicznej.

Mam jeszcze potwierdził to, dla własnej korzyści, tak jak poniżej:

private async void button1_Click(object sender, EventArgs e) 
{ 
    var value1 = await Task.Run(async() => 
     { 
      await Task.Delay(5000); 
      return "Hello"; 
     }); 

    //NOTE: this one is not awaited... 
    var value2 = Task.Run(async() => 
     { 
      await Task.Delay(5000); 
      return value1.Substring(0, 3); 
     }); 

    System.Diagnostics.Debug.Print(value2.Result); //thus, UI freezes here after 5000 ms. 
} 

Ale teraz zastanawiam się: czy trzeba await wszystko „awaitable” operacji zagnieżdżonych wewnątrz najbardziej zewnętrznej awaitable operacja? Na przykład, mogę to zrobić:

private async void button1_Click(object sender, EventArgs e) 
{ 
    var value0 = await Task.Run(() => 
     { 
      var value1 = new Func<Task<string>>(async() => 
      { 
       await Task.Delay(5000); 
       return "hello"; 
      }).Invoke(); 

      var value2 = new Func<string, Task<string>>(async (string x) => 
       { 
        await Task.Delay(5000); 
        return x.Substring(0, 3); 
       }).Invoke(value1.Result); 

      return value2; 
     }); 

    System.Diagnostics.Debug.Print(value0); 
} 

Można zrobić to:

private async void button1_Click(object sender, EventArgs e) 
{ 
    //This time the lambda is async... 
    var value0 = await Task.Run(async() => 
     { 
      //we're awaiting here now... 
      var value1 = await new Func<Task<string>>(async() => 
      { 
       await Task.Delay(5000); 
       return "hello"; 
      }).Invoke(); 

      //and we're awaiting here now, too... 
      var value2 = await new Func<string, Task<string>>(async (string x) => 
       { 
        await Task.Delay(5000); 
        return x.Substring(0, 3); 
       }).Invoke(value1); 

      return value2; 
     }); 

    System.Diagnostics.Debug.Print(value0); 
} 

i żadna z nich zamrozić UI. Który z nich jest lepszy?

+0

Byłbym bardzo zaskoczony, gdyby ci wszyscy wydrukować to samo, wierzę, że ktoś drukuje obiekt Task zamiast jego wyniku. –

+0

@BenVoigt - Nie, bc w obu przypadkach zadanie jest rozpakowywane, ponieważ jest oczekiwane. –

+0

Ale jednym z nich jest owijanie drugiego zadania. –

Odpowiedz

13

ostatni z nich jest korzystny (chociaż bardzo brudne)

W TAP (Zadanie oparte asynchroniczny wzorzec) Zadanie (i innych awaitables) stanowią asynchroniczne. Masz w zasadzie 3 opcje obsługi tych zadań:

  • Wait synchronicznie (DoAsync().Result, DoAsync().Wait()) - blokuje wywołującego wątku, aż zadanie zostanie zakończone. Sprawia, że ​​twoja aplikacja jest bardziej marnotrawna, mniej skalowalna, mniej responsywna i podatna na zakleszczenia.
  • Poczekaj asynchronicznie (await DoAsync()) - Nie blokuje wywoływanego wątku. Zasadniczo rejestruje pracę po await jako kontynuację do wykonania po zakończeniu oczekiwanego zadania.
  • Nie czekaj w ogóle (DoAsync()) - Nie blokuje wywoływanego wątku, ale także nie czeka na zakończenie operacji. Jesteś świadoma wszelkich wyjątków rzucony podczas DoAsync jest przetwarzany

Konkretnie, zdaję sobie sprawę, że nie czekając na kolejny długotrwałych operacji blokuje wątku UI

Więc nie do końca. Jeśli w ogóle nie zaczekasz, nic nie zostanie zablokowane, ale nie będziesz wiedział, kiedy i czy operacja zakończy się pomyślnie. Jeśli jednak będziesz synchronicznie czekać, zablokujesz wywołujący wątek i możesz mieć zakleszczenia, jeśli blokujesz wątek interfejsu użytkownika.

Wniosek: Powinieneś await swoich oczekujących tak długo, jak to możliwe (nie jest to na przykład w Main). Obejmuje to "operacje zagnieżdżone async-await".

O konkretnym przykładzie: Task.Run służy do odciążenia pracy związanej z procesorem z wątkiem ThreadPool, który nie jest tym, co próbujesz naśladować.Jeśli używamy Task.Delay reprezentować prawdziwie asynchronicznej operacji (zazwyczaj I/O-związany), możemy mieć „zagnieżdżone async-await” bez Task.Run:

private async void button1_Click(object sender, EventArgs e) 
{ 
    var response = await SendAsync(); 
    Debug.WriteLine(response); 
} 

async Task<Response> SendAsync() 
{ 
    await SendRequestAsync(new Request()); 
    var response = await RecieveResponseAsync(); 
    return response; 
} 

async Task SendRequestAsync(Request request) 
{ 
    await Task.Delay(1000); // actual I/O operation 
} 

async Task<Response> RecieveResponseAsync() 
{ 
    await Task.Delay(1000); // actual I/O operation 
    return null; 
} 

Można używać anonimowych delegatów zamiast metod, ale jest to niewygodne, gdy trzeba aby zdefiniować typy i wywołać je samemu.

Jeśli trzeba odciążyć tę operację do ThreadPool wątku, wystarczy dodać Task.Run:

private async void button1_Click(object sender, EventArgs e) 
{ 
    var response = await Task.Run(() => SendAsync()); 
    Debug.WriteLine(response); 
} 
+0

Właściwie to jeszcze raz przeczytałem twoją odpowiedź i zastanawiam się, czy mógłbyś proszę wyjaśnić: czy mówisz, że ja * nie powinnam * używać zagnieżdżonych oczekiwań (co robi mój trzeci blok kodu - ten po "Lub mogę to zrobić")? –

+0

@roryap Mówię, że jeśli operacje są "asynchroniczne", to "zaczekaj" je zamiast blokować bez względu na to, gdzie się znajdują. O tym, czy użyć 'Task.Run' zero, raz czy dwa razy to zależy od tego, co faktycznie robisz * faktycznie * próbujesz zrobić (zakładam, że' Task.Delay' tutaj jest tylko zamiennikiem). – i3arnon

+1

@roryap Jeśli masz długą operację intensywnego procesora, którą chcesz odciążyć, użyj 'Task.Run', w przeciwnym razie po prostu wywołaj operacje' async' i 'await' je. – i3arnon