2014-11-28 20 views
9

Ich bin auf ein Problem mit einem Komponententest gestoßen, der fehlgeschlagen ist, weil ein TPL-Task seinen ContinueWith(x, TaskScheduler.FromCurrentSynchronizationContext()) nie ausgeführt hat.Warum kann Task.ContinueWith in diesem Komponententest nicht ausgeführt werden?

Das Problem stellte sich heraus, weil ein Winforms UI Control versehentlich erstellt wurde, bevor der Task gestartet wurde.

Hier ist ein Beispiel, das es reproduziert. Wenn Sie den Test unverändert ausführen, wird er ausgeführt. Wenn Sie den Test mit der unkommentierten Form-Zeile ausführen, schlägt er fehl.

[TestClass] 
public class UnitTest1 
{ 
    [TestMethod] 
    public void TestMethod1() 
    { 
     // Create new sync context for unit test 
     SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); 

     var waitHandle = new ManualResetEvent(false); 

     var doer = new DoSomethinger(); 

     //Uncommenting this line causes the ContinueWith part of the Task 
     //below never to execute. 
     //var f = new Form(); 

     doer.DoSomethingAsync(() => waitHandle.Set()); 

     Assert.IsTrue(waitHandle.WaitOne(10000), "Wait timeout exceeded."); 
    } 
} 


public class DoSomethinger 
{ 
    public void DoSomethingAsync(Action onCompleted) 
    { 
     var task = Task.Factory.StartNew(() => Thread.Sleep(1000)); 

     task.ContinueWith(t => 
     { 
      if (onCompleted != null) 
       onCompleted(); 

     }, TaskScheduler.FromCurrentSynchronizationContext()); 
    } 
} 

Kann mir jemand erklären, warum dies der Fall ist?

dachte ich es, weil die falsch gewesen sein könnteSynchronizationContext verwendet wird, aber tatsächlich, die ContinueWithnie überhaupt ausgeführt wird! Und außerdem ist in diesem Komponententest, ob es richtig ist oder nicht SynchronizationContext ist irrelevant, denn solange der waitHandle.set() auf einem Thread aufgerufen wird, sollte der Test bestehen.

+0

Tritt eine Ausnahme auf? –

+0

@SriramSakthivel Stupid Frage - Sie haben versucht mit der 'var f = new Form();' UNkommentiert? Ich ein MsTest, VS2013u4, Win8.1, Net 4.5.1 – OffHeGoes

+0

@OffHeGoes Sorry, dass ich das übersehen. Schreibe jetzt eine Antwort :) –

Antwort

7

ich die Kommentare im Code übersah, nämlich, dass wenn uncommenting versagt der var f = new Form();

Grund subtil ist, wird Control Klasse automatisch den Synchronisationskontext zu WindowsFormsSynchronizationContext überschreibt, wenn sie sieht, dass SynchronizationContext.Currentnull oder seine vom Typ ist .

Sobald Control-Klasse die SynchronizationContext.Current mit WindowsFormsSynchronizationContext, alle Anrufe zu Send und Post überschrieben werden erwartet, dass die Windows-Nachrichtenschleife zu arbeiten, um zu laufen. Das wird nicht passieren, bis Sie die Handle erstellt haben und Sie eine Nachrichtenschleife ausführen.

Relevante Teil des problematischen Code:

internal Control(bool autoInstallSyncContext) 
{ 
    ... 
    if (autoInstallSyncContext) 
    { 
     //This overwrites your SynchronizationContext 
     WindowsFormsSynchronizationContext.InstallIfNeeded(); 
    } 
} 

Sie die Quelle WindowsFormsSynchronizationContext.InstallIfNeededhere verweisen. Wenn Sie die SynchronizationContext überschreiben möchten, benötigen Sie Ihre benutzerdefinierte Implementierung SynchronizationContext, damit es funktioniert.

Umgehung:.

internal class MyContext : SynchronizationContext 
{ 

} 

[TestMethod] 
public void TestMethod1() 
{ 
    // Create new sync context for unit test 
    SynchronizationContext.SetSynchronizationContext(new MyContext()); 

    var waitHandle = new ManualResetEvent(false); 

    var doer = new DoSomethinger(); 
    var f = new Form(); 

    doer.DoSomethingAsync(() => waitHandle.Set()); 

    Assert.IsTrue(waitHandle.WaitOne(10000), "Wait timeout exceeded."); 
} 

obige Code funktioniert wie erwartet :)

Alternativ Sie WindowsFormsSynchronizationContext.AutoInstall-false einstellen könnte, das automatische Überschreiben des Synchronisationskontextes oben genannten verhindern (Danke für OP @OffHeGoes für die Erwähnung in den Kommentaren)

+0

Ah, ich denke, ich weiß, was ich falsch gemacht habe - Ich habe 'TaskScheduler.Current' überprüft, als ich versuchte, das Problem zu debuggen - ich hätte' SynchronizationContext.Current' überprüfen sollen! – OffHeGoes

+0

Das ist wirklich nützlich zu wissen. Ich habe gerade das 'WindowsFormsSynchronizationContext.InstallIfNeeded()' betrachtet und kann sehen, dass es nach 'typeof (SynchronizationContext)' sucht. – OffHeGoes

+1

Auch bemerkt, dass Sie dies tun können, um es auch zu stoppen: 'WindowsFormsSynchronizationContext.AutoInstall = false;' – OffHeGoes

3

Wenn die Zeile auskommentiert ist, ist Ihr SynchronizationContext der Standard, den Sie erstellt haben. Dies führt dazu, dass TaskScheduler.FromCurrentSynchrozisationContext() den Standard-Scheduler verwendet, der die Fortsetzung im Thread-Pool ausführt.

Sobald Sie ein Winforms-Objekt wie Ihr Formular erstellen, wird der aktuelle SynchronizationContext zu einem WindowsFormsSynchronizationContext, der wiederum einen Scheduler zurückgibt, der von der WinForms-Nachrichtenpumpe abhängt, um die Fortsetzung zu planen.

Da in einem Komponententest keine WinForms-Pumpe vorhanden ist, wird die Fortsetzung nie ausgeführt.

+0

Danke - Ich habe 'TaskScheduler.Current' überprüft, als ich versuchte, das Problem zu debuggen - ich hätte' SynchronizationContext.Current' überprüfen sollen. – OffHeGoes

Verwandte Themen