Passing Continuations

Last Time we saw how to perform Actions asynchronously and I promised to talk about continuations. Continuations also known as CPS or Continuation Passing Style is a mechanism for passing in code that would normally execute after a function call to the callee function as an argument, perhaps most well known in Scheme.
For example if you were to call a function Sum which returns an int and print out the result like so:

 
  1. public static int Sum(int n)    
  2. {  
  3.       //sure we could use (n*(n+1))/2 but this is just to demonstrate a long running op  
  4.       int sum=0;  
  5.       for(int i=0; i< n; i++)  
  6.       {  
  7.        sum+=i;  
  8.       }  
  9.     return sum;  
  10. }  
  11.    
  12. public static void Main(string[] args)  
  13. {   int num = Sum(3);  
  14.       num++;     
  15.      Console.WriteLine(num);    
  16. }   

You could rewrite this function by passing in a continuation which in this case is everything after the call to Sum(3)

 
  1. public static void Sum(int n, Action<int> action)      
  2. {   //sure we could use (n*(n+1))/2 but this is just to demonstrate a long running op          
  3.     int sum=0;          
  4.    for(int i=0; i< n; i++)          
  5.     {    
  6.       sum+=i;          
  7.     }          
  8.     action(sum);    
  9. }    
  10.   
  11.   
  12. public static void Main(string[] args)  
  13. {  
  14.      Sum(3, n => { n++; Console.WriteLine(n); });  
  15. }  

So the continuation of the computation at the point of invoking Sum is the remainder of the code that would execute after the call to Sum which is Console.WriteLine here. What has this gained us besides just complicating a simple piece of code that was computing Sum.
Nothing much in this contrived example, but if Sum represented a long running computation or if Sum was called repeatedly(as above), then the calling thread will be blocked till each computation completes.
If this thread were the main UI thread of a Winform/WPF application then it becomes even more critical since the UI thread would not be able to process any messages and would appear frozen. Instead if we pass in the code that should execute after the call to Sum completes as an argument to Sum, and invoke the call to Sum asynchronously then this would free up the caller. This is analogous to invoking a delegate asynchronously using the Begin/End Invoke paradigm which essentially uses continuations wherein the delegate that is passed in to EndInvoke represents the continuation that will execute when the delegate on which BeginInvoke is called on completes.

 
  1. public static int Sum(int n)  
  2. {  
  3.     int sum = (n * (n + 1)) / 2;  
  4.     return sum;  
  5. }  
  6.   
  7. public static void Main(string[] args)    
  8. {               
  9.  Func<int,int> sum = Sum;   
  10.         sum.BeginInvoke(3, iasyncResult =>   
  11.                  {  if (iasyncResult.IsCompleted)    
  12.               {                           
  13.          int num = sum.EndInvoke(iasyncResult);  
  14.          num++;  
  15.          Console.WriteLine(num);                       
  16.            }    
  17.                      },null);  
  18. }  
Another possible way of re-writing the above code would be to define a generic Exec function which explicitly accepts the continuation and is responsible to performing the Begin/End Invoke internally, like so:

 
  1. public static void Exec<T>(this Func<T> func, Action<T> continuation)  
  2. {  
  3.     func.BeginInvoke(iasyncResult =>  
  4.         {                                             
  5.             if(iasyncResult.IsCompleted)  
  6.             {  
  7.                 T t = func.EndInvoke(iasyncResult);  
  8.                 continuation(t);  
  9.             }                     
  10.         }, null);  
  11. }  

The client code can now be re-written to use Exec as follows:

 
  1. public static int Sum(int n)     
  2. {          
  3.     //sure we could use (n*(n+1))/2 but this is just to demonstrate a long running op  
  4.     int sum=0;          
  5.     for(int i=0; i< n; i++)          
  6.     {          
  7.         sum+=i;          
  8.     }       
  9.     return sum;    
  10. }    
  11.   
  12. public static void Main(string[] args)  
  13. {  
  14.   
  15.     Func<int> sumfunc1 = () =>  
  16.     {  
  17.         return Sum(5);  
  18.     };  
  19.     sumfunc1.Exec(n => Console.WriteLine("Sum is:{0} using ThreadID:{1}", n, Thread.CurrentThread.ManagedThreadId));  
  20.   
  21.   
  22.     Func<int> sumfunc2 = () =>  
  23.     {  
  24.         return Sum(4);  
  25.     };  
  26.     sumfunc2.Exec(n => Console.WriteLine("Sum is:{0} using ThreadID:{1}", n, Thread.CurrentThread.ManagedThreadId));  
  27.   
  28.   
  29.     Func<int> sumfunc3 = () =>  
  30.     {  
  31.         return Sum(3);  
  32.     };  
  33.     sumfunc3.Exec(n => Console.WriteLine("Sum is:{0} using ThreadID:{1}", n, Thread.CurrentThread.ManagedThreadId));  
  34.   
  35. }  
Hope this helps.

kick it on DotNetKicks.com

Print | posted @ Wednesday, April 15, 2009 11:47 PM

Comments on this entry:

No comments posted yet.

Post A Comment
Title:
Name:
Email:
Comment:
Verification: