2012-05-27 20 views
8

Myślę, że dostrzegłem poważny błąd w TPL. Nie jestem pewien. Spędziłem dużo czasu na drapaniu się po głowie i nie mogę zrozumieć zachowania. Czy ktoś może pomóc?Błąd w TPL - TaskContinuationOptions.ExecuteSynchronously?

Co mój scenariusz jest:

  1. utworzyć zadanie, które wykonuje prostą rzecz. Bez wyjątków itp.
  2. Rejestruję kontynuację z ustawieniem ExecuteSynchronously. Musi być na tym samym wątku.
  3. Uruchamiam zadanie na domyślnym taskcheduler (ThreadPool). Początkowy wątek przebiega i czeka na niego.
  4. Zadanie rozpoczyna się. Karnety.
  5. Kontynuacja rozpoczyna się w tym samym wątku co zadanie (wykonanie poprzedniego zadania!) I wchodzi w nieskończoną pętlę.
  6. Nic się nie dzieje z oczekującym wątkiem. Nie chce iść dalej. Zatrzymany na poczekaniu. Sprawdziłem w debugerze, zadaniem jest RunToCompletion.

Oto mój kod. Doceniamy każdą pomoc!

// note: using new Task() and then Start() to avoid race condition dangerous 
// with TaskContinuationOptions.ExecuteSynchronously flag set on continuation. 

var task = new Task(() => { /* just return */ }); 
task.ContinueWith(
    _task => { while (true) { } /* never return */ }, 
    TaskContinuationOptions.ExecuteSynchronously); 

task.Start(TaskScheduler.Default); 
task.Wait(); // a thread hangs here forever even when EnterEndlessLoop is already called. 
+0

Też myślę, że to jest błąd. Oto strona dokumentacji: http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskcontinuationoptions.aspx Niestety nie mówi nic o tej sprawie. – usr

+0

Czy istnieje możliwość rozszerzenia fragmentu kodu tak, aby inni mogli go skopiować i wkleić? –

+0

@usr - mówi: "Tylko bardzo krótkie kontynuacje powinny być wykonywane synchronicznie." :) Zgadzam się, że to błąd. –

Odpowiedz

5

Zapisano błąd na connect w Twoim imieniu - mam nadzieję, że jest ok :)

https://connect.microsoft.com/VisualStudio/feedback/details/744326/tpl-wait-call-on-task-doesnt-return-until-all-continuations-scheduled-with-executesynchronously-also-complete

Zgadzam się, że jest to błąd, po prostu chciał umieścić fragment kodu, który pokazuje problem, bez konieczności "niekończące się" oczekiwanie. Błąd polega na tym, że funkcja ExecuteSynchronical oznacza, że ​​połączenia oczekujące na pierwszym zadaniu nie powracają, dopóki nie zakończą się również kontynuacje ExecuteSynchronously.

Uruchamianie poniższego fragmentu pokazuje, że czeka 17 sekund (więc wszystkie 3 musiały zostać wypełnione zamiast tylko pierwszego). To samo dotyczy tego, czy trzecie zadanie jest zaplanowane od pierwszego czy drugiego (tak, aby WykonanieSynchroniczne będzie kontynuowane przez drzewo zadań zaplanowanych jako takie).

void Main() 
{ 
    var task = new Task(() => Thread.Sleep(2 * 1000)); 
    var secondTask = task.ContinueWith(
     _ => Thread.Sleep(5 * 1000), 
     TaskContinuationOptions.ExecuteSynchronously); 
    var thirdTask = secondTask.ContinueWith(
     _ => Thread.Sleep(10 * 1000), 
     TaskContinuationOptions.ExecuteSynchronously); 

    var stopwatch = Stopwatch.StartNew(); 
    task.Start(TaskScheduler.Default); 
    task.Wait(); 
    Console.WriteLine ("Wait returned after {0} seconds", stopwatch.ElapsedMilliseconds/1000.0); 
} 

Jedyną rzeczą, która sprawia, że ​​myślę, że może być celowe (a więc bardziej bug doc niż kod błędu) jest Stephen's comment in this blog post:

ExecuteSynchronously jest wniosek o optymalizację, aby uruchomić kontynuacja zadania na tym samym wątku, który zakończył poprzednie zadanie, z tego zadania, które kontynuowaliśmy, w wyniku kontynuacji jako część przejścia poprzedzającego do stan aktywny

0

Okazuje się, że to rzeczywiście błąd. Myślę, że wszyscy się zgadzają. Jeśli jednak miało to wyglądać tak, to API i dokumenty wydają się bardzo mylące.

Praca, której używałem, to po prostu użycie ManualResetEventSlim.

var eventSlim = new ManualResetEventSlim(false); 
var task = new Task(() => { eventSlim.Set(); }); 
task.ContinueWith(
    _task => { while (true) { } /* never return */ }, 
    TaskContinuationOptions.ExecuteSynchronously); 

task.Start(TaskScheduler.Default); 
eventSlim.Wait(); 

Dziękuję wszystkim za obejrzenie tego! I dla wszystkich komentarzy.

Pozdrawiam.

3

To zachowanie ma sens, jeśli wziąć pod uwagę task inlining. Po wywołaniu Task.Wait przed rozpoczęciem wykonywania zadania program planujący spróbuje go wstawić, tj. Uruchomić na tym samym wątku, który wywołał Task.Wait. Ma to sens - po co marnować wątek oczekujący na zadanie, kiedy można ponownie użyć wątku, aby wykonać zadanie?

Teraz, gdy określono ExecuteSynchronously, program planujący jest polecany do wykonania kontynuacji w tym samym wątku, co zadanie poprzedzające - które jest oryginalnym wątkiem wywołującym w przypadku inliningu.

Należy pamiętać, że gdy nie jest stosowane oznaczanie nietypowe, oczekiwane zachowanie: ma miejsce. Jedyne, co musisz zrobić, to zabronić zakładania i to proste - albo określ limit czasu dla czekania lub przekazania tokena anulowania, np.

task.Wait(new CancellationTokenSource().Token); //This won't wait for the continuation 

Na koniec należy pamiętać, że nie jest gwarantowane podciąganie. Na moim komputerze tak się nie stało, ponieważ zadanie zostało już rozpoczęte przed wywołaniem Wait, więc moje połączenie Wait nie zostało zablokowane. Jeśli chcesz odtwarzalny blok, zadzwoń pod numer Task.RunSynchronously.

Powiązane problemy