24

Biorąc pod uwagę następujący kod:Anulowanie żądania HttpClient - Dlaczego TaskCanceledException.CancellationToken.IsCancellationRequested false?

var cts = new CancellationTokenSource(); 

try 
{ 
    // get a "hot" task 
    var task = new HttpClient().GetAsync("http://www.google.com", cts.Token); 

    // request cancellation 
    cts.Cancel(); 

    await task; 

    // pass: 
    Assert.Fail("expected TaskCanceledException to be thrown"); 
} 
catch (TaskCanceledException ex) 
{ 
    // pass: 
    Assert.IsTrue(cts.Token.IsCancellationRequested, 
     "expected cancellation requested on original token"); 

    // fail: 
    Assert.IsTrue(ex.CancellationToken.IsCancellationRequested, 
     "expected cancellation requested on token attached to exception"); 
} 

spodziewałbym ex.CancellationToken.IsCancellationRequested być true wewnątrz bloku catch, ale tak nie jest. Czy coś nie rozumiem?

+0

Czy instancja "ex.CancelationToken" jest równa (ReferenceEqual) dla cts? Dokumentacja stwierdza: "Jeśli token jest powiązany z anulowaną operacją, właściwość tokenu" CancellationToken.IsCancellationRequested "zwraca" true ". – Alex

+2

@Alex: 'CancellationToken' jest strukturą, więc' ReferenceEquals() 'zawsze zwróci false. –

+0

@PeterDuniho twój komentarz sugeruje, że 'object.ReferenceEquals' zwróci' false' dla sprawdzenia structs. Czy implikowałeś to, czy też miałeś na myśli, że logicznie musi to być "fałsz", ponieważ wyniki "IsCancellationRequested" różnią się? –

Odpowiedz

36

To dlatego, że HttpClient wewnętrznie (w SendAsync) używa TaskCompletionSource do reprezentowania operacji async. Zwraca TaskCompletionSource.Task i to jest zadanie, które wykonujesz na await.

Następnie wywołuje base.SendAsync i rejestruje kontynuację zwróconego zadania, które anuluje/zakończy/uszkodzi zadanie TaskCompletionSource.

W przypadku anulowania korzysta z TaskCompletionSource.TrySetCanceled, która kojarzy anulowane zadanie z nowym CancellationToken (default(CancellationToken)).

Możesz to zobaczyć, patrząc na TaskCanceledException. Na szczycie ex.CancellationToken.IsCancellationRequested jest falseex.CancellationToken.CanBeCanceled jest również false, co oznacza, że ​​ten CancellationToken nigdy nie może być anulowane, ponieważ nie został utworzony przy użyciu CancellationTokenSource.


Zamiast tego należy użyć TaskCompletionSource.TrySetCanceled(CancellationToken). W ten sposób TaskCompletionSource zostanie powiązany z CancellationToken przekazanym przez konsumenta, a nie po prostu domyślnym CancellationToken. Myślę, że to błąd (choć drobny) i przesłałem o tym issue on connect.

+2

Świetna odpowiedź, +1 za zgłoszenie problemu. Nie jestem pewien, zgadzam się, że jest to niewielkie, ponieważ ja (i [inni] (http://stackoverflow.com/q/12666922/62600)) polegam na tym, aby odróżnić anulowanie użytkownika od przekroczenia czasu w przypadku 'HttpClient'. –

+0

@ToddMenier To było zaskakująco interesujące pytanie. To również prowadzi mnie do tego interesującego (mam nadzieję) stwierdzenia: http://stackoverflow.com/q/29355165/885318 – i3arnon

+0

Cóż, przeciążenie, o którym myślisz * jest wewnętrzne *, więc prawdopodobnie był jakiś powód, dla którego nie ma do użycia w ten sposób. Podczas anulowania 'TaskCompletionSource' dzieje się wiele interesujących rzeczy (na przykład możesz spróbować anulować TCS wiele razy). I ani TCS, ani "zadanie", które wewnętrznie tworzy, nie mają nic wspólnego z tokenami anulowania - to tylko jedna z tych nieszczelnych abstrakcji. Istnieją naprawdę dwa rodzaje zadań 'Task', Task-as-CPU-work vs. Task-async-I/O,' HttpClient' używające tego drugiego. Czasem żałuję, że nie były oddzielne ... – Luaan

Powiązane problemy