Wednesday, September 1, 2010

Porting C# Code to IronPython, an Example

Lately, I have been reading Jeff Richter’s book “CLR via C#, 3rd Edition.”  I have read several of Jeff Richter’s programming books over the years, mostly his series of books on programming Windows with C/C++.  I have always liked his writing.  He is not afraid to say that Microsoft has made a mistake by providing a Windows API or technology that is substandard.  He won’t just complain about it, no, he will go on to say how he would have done it (and provide code).  Don’t get me wrong here, he also points out many things that Microsoft has done well.  I just respect the fact that he is not afraid to provide a dissenting opinion.

In chapter 26 of “CLR via C#, 3rd, Ed.,” “Compute-Bound Asynchronous Operations”  there is a cool example of using Task objects from the System.Threading.Task namespace.  This small example demonstrates several features of Task objects including using a TaskScheduler to sync back to the GUI thread, cancelling a task, and using a continuation task to execute another action after a task completes.  The sample uses a contrived example of a compute bound function named Sum that keeps the CPU busy for a while by adding integers from 0 through some number n.  This program is a simple Windows Forms application.  I have modified it slightly to use a button instead of just detecting a mouse click on the form.  Below is the form and the C# code that appeared in the book (modified by me).

  form

 1 using System;
2 using System.Windows.Forms;
3 using System.Threading;
4 using System.Threading.Tasks;
5
6 namespace TaskSchedTest
7 {
8 public partial class Form1 : Form
9 {
10 private readonly TaskScheduler m_syncContextTaskScheduler;
11 private CancellationTokenSource m_cts;
12
13 public Form1()
14 {
15 m_syncContextTaskScheduler =
16 TaskScheduler.FromCurrentSynchronizationContext();
17
18 InitializeComponent();
19 }
20
21 private void button1_Click(object sender, EventArgs e)
22 {
23 if (null != m_cts)
24 {
25 m_cts.Cancel();
26 }
27 else
28 {
29 label1.Text = "Operation Running...";
30 button1.Text = "Cancel Task";
31
32 // Define a function to reset the state of the program
33 // upon task completion.
34 Func<String, Int32> reset = (String labelText) =>
35 {
36 label1.Text = labelText;
37 m_cts = null;
38 button1.Text = "Run Task";
39 return 0;
40 };
41
42 m_cts = new CancellationTokenSource();
43
44 // This task uses the default task scheduler and executes
45 // on a thread pool thread.
46 var t = new Task<Int64>(() => Sum(m_cts.Token, 200000000), m_cts.Token);
47 t.Start();
48
49 // These tasks use the syn context task schedules and execute
50 // on the GUI thread.
51 t.ContinueWith(task => { reset("Result: " + task.Result); },
52 CancellationToken.None,
53 TaskContinuationOptions.OnlyOnRanToCompletion,
54 m_syncContextTaskScheduler);
55
56 t.ContinueWith(task => { reset("Operation canceled"); },
57 CancellationToken.None,
58 TaskContinuationOptions.OnlyOnCanceled,
59 m_syncContextTaskScheduler);
60
61 t.ContinueWith(task => { reset("Operation faulted"); },
62 CancellationToken.None,
63 TaskContinuationOptions.OnlyOnFaulted,
64 m_syncContextTaskScheduler);
65 }
66 }
67
68 private static Int64 Sum(CancellationToken ct, Int32 n)
69 {
70 Int64 sum = 0;
71 for (; n > 0; n--)
72 {
73 // The following throws OperationCanceledException when Cancel
74 // is called on the CancellationTokenSource referred by the token
75 ct.ThrowIfCancellationRequested();
76 checked { sum += n; }
77 }
78
79 return sum;
80 }
81 }
82 }



I added the reset function that is used within the lambda expressions in the ContinueWith method calls.  I like this code because it is very succinct and it doesn’t pollute your class namespace with a bunch of private functions that are only used within this one method call.  The performance hit of creating the reset function and the lambda expressions are negligible as well.  The C# compiler actually generates an internal class that contains as method members the lambda expression functions.  You can see this in the image below taken from ildasm.exe.  The compiler generated class is called <>c__DisplayClass7 and the lambda expressions from the ContinueWith method calls are named <button1_Click>b__2, <button1_Click>b__3, and <button1_Click>b__4. You can also see the reset function object as a field.       



ildasm1



I liked this sample so much that I wanted to port it to IronPython.  I enjoy Python programming and I have been trying to incorporate IronPython into my work whenever it makes sense to do so.  Being that Python and IronPython are dynamic languages, interfacing the to the .NET Framework can make the syntax cumbersome and not very Pythonic at times.  You must ensure that your IronPython code is using the correct types because the .NET Framework is statically typed. A lot of the time the IronPython interpreter will infer the correct types for you and everything just works.  At other times, the IronPython interpreter does not infer the correct types and you are left with a runtime exception.  For this IronPython example I changed to a Windows Presentation Foundation application, mostly because Visual Studio has a drag and drop WPF editor for IronPython.  Below is the WPF form with XAML and the IronPython code.



wpf





xaml




 1 import clr
2 clr.AddReference('PresentationFramework')
3
4 from System import Func
5 from System.Windows import Application, Window
6 from System.Threading import CancellationTokenSource, CancellationToken
7 from System.Threading.Tasks import (Task, TaskScheduler,
8 TaskContinuationOptions
9 )
10
11 class MyWindow(Window):
12 def __init__(self):
13 clr.LoadComponent('WpfApplication1.xaml', self)
14 self.cts = None
15
16 def AppLoaded(self, sender, e):
17 self.syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()
18
19 def Button_Click(self, sender, e):
20 if self.cts is not None:
21 self.cts.Cancel()
22 else:
23 self.label1.Content = "Operation Running...";
24 self.button1.Content = "Cancel Task";
25
26 # Define a function to reset the state of the program
27 # upon task completion.
28 def reset(labelText):
29 self.label1.Content = labelText
30 self.button1.Content = "Run Task"
31 self.cts = None
32
33 self.cts = CancellationTokenSource();
34
35 # This task uses the default task scheduler and executes
36 # on a thread pool thread.
37 t = Task[long](lambda: self.Sum(self.cts.Token, 2000000), self.cts.Token)
38 t.Start()
39
40 NoneType = type(None)
41 # These tasks use the syn context task schedules and execute
42 # on the GUI thread.
43 t.ContinueWith[NoneType](
44 Func[Task[long], NoneType](
45 lambda task: reset("Result: {0}".format(task.Result))
46 ),
47 CancellationToken.None,
48 TaskContinuationOptions.OnlyOnRanToCompletion,
49 self.syncContextTaskScheduler)
50
51 t.ContinueWith[NoneType](
52 Func[Task[long], NoneType](
53 lambda task: reset("Operation canceled.")
54 ),
55 CancellationToken.None,
56 TaskContinuationOptions.OnlyOnCanceled,
57 self.syncContextTaskScheduler)
58
59 t.ContinueWith[NoneType](
60 Func[Task[long], NoneType](
61 lambda task: reset("Operation faulted.")
62 ),
63 CancellationToken.None,
64 TaskContinuationOptions.OnlyOnFaulted,
65 self.syncContextTaskScheduler)
66
67 @staticmethod
68 def Sum(cancellationToken, n):
69 sum = 0L
70 for i in xrange(n + 1):
71 cancellationToken.ThrowIfCancellationRequested()
72 sum += i
73 return sum
74
75
76 if __name__ == '__main__':
77 Application().Run(MyWindow())



The most difficult part of this port was getting the .NET Framework generic types correct.  When constructing the initial Task object, I had to use the Task[long] notation for generics with IronPython.  Without the generic parameter, the IronPython interpreter would produce a non-generic Task object and this version of the Task object does not have a Result property as the generic parameter is the result type (line 37).  The nice thing about this line is that I could pass in a Python lambda expression directly and the interpreter infers the proper .NET type, Func[long], in this case.  As you can see later in the code this is not the case.  For those that do not know, generic syntax in IronPython differs from C#. For example: Task<TResult>, in C# you may have Task<Int64> where in IronPython you would write Task[long]. 



The ContinueWith method calls on lines 43, 51, and 59 gave me the most trouble.  I found that if I tried to pass the lambda expressions directly as the first parameter to ContinueWith, the IronPython interpreter would generate a Func[Task, long] object, when in fact I needed to have Func[Task[long], NoneType] objects.  This is because each of these lambda expressions are called with a single parameter of type Task[long] and they do not have a return value.  In C# this would be void, IronPython it would be NoneType.  To make this work, I had to explicitly create Func[Task[long], NoneType] objects and pass them into ContinueWith.  Luckily I could pass the Python lambda expressions directly to the Func constructor.  I also used the generic notation on the ContinueWith[NoneType] calls to state that they do not have any return value. 



When I first coded this up I had mistakenly used Func[Task[long], long] objects and ContinueWith[long] method calls.  This is stating that the functions will take a Task[long] parameter (which is correct) and return a long value (which is not correct).  This actually seemed to work, at least until the Finalizer thread ran!  When the Func[Task[long], long] object was called by the framework, an exception was thrown because it was expecting a long value to be returned but received NoneType.  It seemed to work because my function had already executed.  The Finalizer thread would see that a Task had thrown an exception.  It would then pack up this exception into an AggregateException and throw that object. 



I had fun porting this sample to IronPython and I hope to use it more in my daily work.  Interfacing with the .NET Framework with IronPython can be a very non-Pythonic experience, but this just how it is when you cross the dynamic to static type boundary.

No comments:

Post a Comment