Udostępnij za pośrednictwem


CCR tips and tricks - part 9

The pattern in CCR to use iterators to implements tasks is very powerful since it means that the code of the task looks very synchronous even though it performs asynchronous tasks. But sometimes you may yield on the wrong thing and the resulting behavior is unexpected. But even before you use an iterative handler (as they're called in CCR) you should ask yourself if you really need it. The rule of thumb is fairly easy. If you need a "yield break" in the end of your method to make it compile (since there are no "yield return" in it) you should not use an iterative handler. The exception is if you implement some interface or base implementation that others will override and where you anticipate those implementations to need an iterative handler. The reason to not use an iterative handler unless needed is that there is a slight performance penalty from using iterative handlers so if you don't need them, don't pay the price.

In the following code examples I will be reusing the following simple methods/variables (and yes I have an iterative handler that should not be it, but it's used to show a difference between iterative handlers and regular handlers.

   1: private DispatcherQueue dispatcherQueue = new DispatcherQueue();
  2: private bool didSomething = false;
  3: private bool didSomethingElse = false;
  4:  
  5: private void DoSomething()
  6: {
  7:     didSomething = true;
  8: }
  9:  
 10: private IEnumerator<ITask> DoSomethingElse()
 11: {
 12:     didSomethingElse = true;
 13:     yield break;
 14: }

Now look at the following test. Notice that the iterative task is never executed. This is because yielding on a regular task executes that task but never returns to the original method.

   1: private IEnumerator<ITask> DoItAllWrongWay(ManualResetEvent mre)
  2: {
  3:     yield return Arbiter.FromHandler(DoSomething);
  4:     yield return Arbiter.FromIteratorHandler(DoSomethingElse);
  5:     mre.Set();
  6: }
  7:  
  8: [TestMethod]
  9: public void Doing_the_wrong_thing()
 10: {
 11:     var mre = new ManualResetEvent(false);
 12:     Arbiter.Activate(
 13:         dispatcherQueue, 
 14:         Arbiter.FromIteratorHandler(() => this.DoItAllWrongWay(mre)));
 15:     Assert.IsFalse(
 16:         mre.WaitOne(TimeSpan.FromSeconds(3)), 
 17:         "Should not complete tasks");
 18:     Assert.IsTrue(didSomething);
 19:     Assert.IsFalse(didSomethingElse);
 20: }

Now look at what happens when we yield return on the iterative handler first instead. Now both helper methods execute but we still don't complete the calling method since yielding on the regular handler never returns.

   1: private IEnumerator<ITask> DoItWrongButItWorksAlmost(ManualResetEvent mre)
  2: {
  3:     yield return Arbiter.FromIteratorHandler(DoSomethingElse);
  4:     yield return Arbiter.FromHandler(DoSomething);
  5:     mre.Set();
  6: }
  7:  
  8: [TestMethod]
  9: public void Doing_the_wrong_thing_but_it_almost_works()
 10: {
 11:     var mre = new ManualResetEvent(false);
 12:     Arbiter.Activate(
 13:         dispatcherQueue, 
 14:         Arbiter.FromIteratorHandler(() => this.DoItWrongButItWorksAlmost(mre)));
 15:     Assert.IsFalse(
 16:         mre.WaitOne(TimeSpan.FromSeconds(3)), 
 17:         "Should not complete tasks");
 18:     Assert.IsTrue(didSomething);
 19:     Assert.IsTrue(didSomethingElse);
 20: }

Last we'll look at the correct way of dealing with this scenario.

   1: private IEnumerator<ITask> DoItRight(ManualResetEvent mre)
  2: {
  3:     yield return Arbiter.ExecuteToCompletion(
  4:         dispatcherQueue, new Task(DoSomething));
  5:     yield return new IterativeTask(DoSomethingElse);
  6:     mre.Set();
  7: }
  8:  
  9: [TestMethod]
 10: public void Doing_the_right_thing()
 11: {
 12:     var mre = new ManualResetEvent(false);
 13:     Arbiter.Activate(
 14:         dispatcherQueue, 
 15:         Arbiter.FromIteratorHandler(() => this.DoItRight(mre)));
 16:     Assert.IsTrue(mre.WaitOne(TimeSpan.FromSeconds(3)), "Should complete tasks");
 17:     Assert.IsTrue(didSomething);
 18:     Assert.IsTrue(didSomethingElse);
 19: }

Note that for iterative tasks you may actually use an alternative syntax (line 5) when yielding to an iterative task.