13

(Jest to nowa próba na to pytanie, które teraz pokazuje problem lepiej).Jak zachować oczekiwanie na zachowanie z TaskCompletionSource.SetException?

Powiedzmy mamy nic zarzucić zadania (var faultedTask = Task.Run(() => { throw new Exception("test"); });), a my go czekają. await rozpakuje AggregateException i wyrzuci podstawowy wyjątek. To rzuci faultedTask.Exception.InnerExceptions.First().

Zgodnie z kodem źródłowym dla ThrowForNonSuccess zrobi to, wykonując każdy zapisany ExceptionDispatchInfo, prawdopodobnie w celu zachowania ładnych śladów stosu. To nie rozpakuje AggregateException, jeśli nie ma ExceptionDispatchInfo.

Już sam ten fakt był zaskoczeniem dla mnie, ponieważ dokumentacja wskazuje, że pierwszy wyjątek jest zawsze rzucony: https://msdn.microsoft.com/en-us/library/hh156528.aspx?f=255&MSPPError=-2147217396 Okazuje się, że await może rzucać AggregateException, choć, co nie jest udokumentowane zachowanie.

Staje się to problemem, gdy chcemy utworzyć zadanie proxy i ustawić to wyjątek:

var proxyTcs = new TaskCompletionSource<object>(); 
proxyTcs.SetException(faultedTask.Exception); 
await proxyTcs.Task; 

To rzuca AggregateException natomiast await faultedTask; byłby rzucony wyjątek testową.

Jak mogę utworzyć zadanie proxy, które mogę wykonać w dowolnym momencie i które będzie odzwierciedlało zachowanie wyjątku, które miało oryginalne zadanie?

Oryginalny zachowanie:

  1. await rzuci pierwszy wyjątek wewnętrzną.
  2. Wszystkie wyjątki są nadal dostępne za pośrednictwem Task.Exception.InnerExceptions. (. Wcześniejsza wersja tej kwestii pominąć ten wymóg)

Oto test, który podsumowuje wyniki:

[TestMethod] 
public void ExceptionAwait() 
{ 
    ExceptionAwaitAsync().Wait(); 
} 

static async Task ExceptionAwaitAsync() 
{ 
    //Task has multiple exceptions. 
    var faultedTask = Task.WhenAll(Task.Run(() => { throw new Exception("test"); }), Task.Run(() => { throw new Exception("test"); })); 

    try 
    { 
     await faultedTask; 
     Assert.Fail(); 
    } 
    catch (Exception ex) 
    { 
     Assert.IsTrue(ex.Message == "test"); //Works. 
    } 

    Assert.IsTrue(faultedTask.Exception.InnerExceptions.Count == 2); //Works. 

    //Both attempts will fail. Uncomment attempt 1 to try the second one. 
    await Attempt1(faultedTask); 
    await Attempt2(faultedTask); 
} 

static async Task Attempt1(Task faultedTask) 
{ 
    var proxyTcs = new TaskCompletionSource<object>(); 
    proxyTcs.SetException(faultedTask.Exception); 

    try 
    { 
     await proxyTcs.Task; 
     Assert.Fail(); 
    } 
    catch (Exception ex) 
    { 
     Assert.IsTrue(ex.Message == "test"); //Fails. 
    } 
} 

static async Task Attempt2(Task faultedTask) 
{ 
    var proxyTcs = new TaskCompletionSource<object>(); 
    proxyTcs.SetException(faultedTask.Exception.InnerExceptions.First()); 

    try 
    { 
     await proxyTcs.Task; 
     Assert.Fail(); 
    } 
    catch (Exception ex) 
    { 
     Assert.IsTrue(ex.Message == "test"); //Works. 
    } 

    Assert.IsTrue(proxyTcs.Task.Exception.InnerExceptions.Count == 2); //Fails. Should preserve both exceptions. 
} 

Motywacja na to pytanie jest to, że próbuję skonstruować funkcję skopiuje wynik jednego zadania na TaskCompletionSource. Jest to funkcja pomocnicza, która jest często używana podczas pisania funkcji kombinatorów zadań. Ważne jest, aby klienci korzystający z interfejsu API nie mogli wykryć różnicy między pierwotnym zadaniem a zadaniem proxy.

+1

To zawsze jest zabawa, aby przeczytać nowe pytanie z 50k + użytkownik rep, 90% czas mi pójść "ooh, chciałbym też o tym wiedzieć" i skończyć na faworyzowaniu pytania –

+0

Jeśli chodzi o semantykę "oczekuj", której szukasz w odniesieniu do wyjątków, dlaczego nie po prostu pójść prosto do źródła (http: // referencesource .microsoft.com/# mscorlib/system/runtime/compilerservices/TaskAwaiter.cs, ca9850c71672bd54) i skopiować, co robi 'TaskAwaiter'? –

+0

@ Kirir Shlenskiy wykorzystuje to wewnętrzne interfejsy API. Ponadto osoba wywołująca mój kod nie musi znać niczego specjalnego. – usr

Odpowiedz

8

Okazuje się, że oczekiwanie może rzucić wyjątek AggregateException, który nie jest udokumentowanym zachowaniem.

Nie, to zachowanie, gdy pierwszy wyjątek zagnieżdżonych jestAggregateException.

Zasadniczo, po wywołaniu TaskCompletionSource.SetException(Exception), opakowuje ten wyjątek w AggregateException.

Jeśli chcesz zachować stwardnienie wyjątki, wystarczy użyć przeciążenie SetException który akceptuje IEnumerable<Exception>:

proxyTcs.SetException(faultedTask.Exception.InnerExceptions);