The Symphony add-in and extensions are automatically installed in Visual Studio during installation of the Symphony DE package when the Visual Studio add-in option is selected. Visual Studio 2008 or 2010 must be installed on the development host prior to installing Symphony DE.
Two on-boarding application samples are provided with the Symphony DE package. One sample is a basic calculator program that calculates the interest for different principal amounts, interest rates, and durations. This sample is organized into a main program and a separate class library where the calculations are performed. In the context of Symphony, the main program and class library represent the client and service, respectively. This sample also serves as a foundation for this tutorial as you prepare it for on-boarding onto the grid.
The second sample reflects the same application as in the first sample, but it has already been updated and grid-enabled. This sample shows you what your first sample should look like once you have completed the on-boarding process. Use this sample to see how calculations are performed on the grid or as a handy reference if you encounter any difficulty on-boarding the first sample.
The sample applications are located at:
%SOAM_HOME%\5.1\samples\AppOnboarding\DotNet\CS\BasicCalculator.
This tutorial refers to the sample applications included in the Symphony DE package at:
This tutorial was prepared using the Visual Studio 2008 Professional version but it should equally apply to Visual Studio 2010 Professional; any differences between the two Visual Studio versions relevant to this tutorial will be noted.
This section describes the steps for on-boarding an existing C#.NET application using the Symphony project wizard in Visual Studio.
This step applies only to the Visual Studio 2010 IDE. If you are using Visual Studio 2008, proceed to Step 5.
As of .NET runtime 4.0, a change was made to no longer load mixed assemblies automatically. In order to load mixed assemblies, the runtime must be explicitly instructed through the use of a configuration file.Since all on-boarded applications still rely on the Symphony API (which is implemented as a mixed assembly), both the client and the service package to be deployed to the grid must be associated with a configuration file to instruct the runtime to load the mixed assembly.. This means that as long as a .Net binary is being loaded by the .NET 4.0 framework, as a minimum, the startup attribute useLegacyV2RuntimeActivationPolicy must be set to true in the configuration file. For convenience, we will use the configuration files that are packaged with the sample.
It is good programming practice to free up Symphony resources when they are no longer needed. For example, the client may have used a proxy and has no intention of using it anymore but the client is expected to run for several minutes after that. During this time, the session would remain open unnecessarily in Symphony. To address this issue, we can call the Dispose() method on the proxy object at a point where we no longer need the proxy and just before the reference goes out of scope.
...Calculate(principalAmounts, calculator);}catch (Exception E){Console.WriteLine("{0}\n{1}", E.Message, E.StackTrace);}finally{if (null != calculator){calculator.Dispose();}}
Refer to Simplified application on-boarding with Visual Studio in the Application Development Guide for more information about the Dispose() method.
Next, we will look at how to optimize the client code to maximize the benefits of running our application on the grid.
In the previous step where we ran our application on the grid, our client interacted with the grid-enabled library and the calculations were actually performed on the grid. But the interaction between the client and the service was synchronous in nature as each input task was sent to the service, one at a time, in a blocking mode.
The following code snippet was taken from the client file Program.cs.
foreach (double amount in principalAmounts){double interest = calculator.SimpleInterest(amount);Console.WriteLine("Principal : ${0},\t Interest: ${1}",amount, interest);}
In the code, we can see that the main execution thread loops through the principal amounts, one at a time, and waits for the result of each calculation to return before it calls the next method. Although the code executes and results are collected, this approach does not benefit from running on the grid. Benefits can only be realized when you execute methods in parallel across multiple computing resources available on the grid.
One of the simple ways to run our calculations in parallel is by using the .NET Asynchronous Programming Model (APM) convention and break our loop into two steps that use the appropriate methods to begin and end our calculation methods. In the code snippet from the previous section, instead of using the SimpleInterest() method on our proxy, we will use the BeginSimpleInterest() method that will not block our execution thread. This will allow us to submit multiple calculations, wait for each one to complete, and then collect the results from our EndSimpleInterest() method. Using the APM model, we can use a callback to collect the results as well.
Now let’s review the same client code as before but this time we have updated it to use APM to perform the calculations in parallel. (Note that this code is taken from the Visual Studio solution at %SOAM_HOME%\5.1\samples\AppOnboarding\DotNet\CS\BasicCalculator\2-After.)
public static void CalculateInParallel(List<double>principalAmounts, BasicGridCalculator.BasicCalculator calculator){...// Begin calculations in parallelList<IAsyncResult> results = newList<IAsyncResult>(principalAmounts.Count);foreach (double amount in principalAmounts){IAsyncResult result =calculator.BeginSimpleInterest(amount, null, amountas Object);results.Add(result);}foreach (IAsyncResult result in results){result.AsyncWaitHandle.WaitOne();double interest = calculator.EndSimpleInterest(result);double amount = (double)result.AsyncState;Console.WriteLine("Principal : ${0},\t Interest:${1}", amount, interest);}}
To perform the calculations in parallel, first we need to set up a list of IAsyncResult objects to hold the results as they are collected asynchronously. The IAsyncResult objects store information about each interest calculation to be performed in the loop.. We will use this information to help us retrieve the results later in the second loop.
The BeginSimpleInterest() method takes three arguments: the principal amount, null (since we are not using any callback), and the principal amount typed as an object (we are using this as a convenient way to preserve the principal amount so we can retrieve it later when we print the output).
After calling BeginSimpleInterest(), the main thread continues execution while the asynchronous interest calculation takes place on the grid in the service process. Once executed, the BeginSimpleInterest() immediately returns a reference that is added to the results list so that we can keep track of the corresponding result. The main thread cycles through the loop repeating the same sequence for each principal amount.
Once all the asynchronous interest calculations are done, a second loop retrieves the results. When we reach result.AsyncWaitHandle.WaitOne(), the main thread is blocked until the specific result is available. We retrieve the result by passing the result reference to the EndSimpleInterest() method. We also retrieve the original principal amount from the AsyncState property of the result object.
In this sample, we review how to perform the calculations in parallel but this time we collect the results using a callback to let us know when the results are ready. The following sample code is taken from the Visual Studio solution at %SOAM_HOME%\5.1\samples\AppOnboarding\DotNet\CS\BasicCalculator\2-After.
public static void CalculateInParallelWithCallback(List<double>principalAmounts, BasicGridCalculator.BasicCalculator calculator){lock (resultLock){outstandingMethodResults = principalAmounts.Count;}// Begin calculations in parallel (with a callback)foreach (double amount in principalAmounts){SimpleInterestMethodContext ctx =new SimpleInterestMethodContext(calculator, amount);calculator.BeginSimpleInterest(amount,SimpleInterestCompleted, ctx as Object);}outstandingMethodsCompleted.WaitOne();outstandingMethodsCompleted.Reset();}
The first step is to initialize outstandingMethodResults to the number of expected results. We will use this variable to keep track of the number of results collected so that we know when all the work is done.
This time we construct a context object for each call in the loop since we will be processing the result in our callback method. As the callback will be called from a different thread, it is important to maintain a reference to our proxy so that we can still collect the results when the SimpleInterest() method completes later.
We pass three arguments to the BeginSimpleInterest() method: the principal amount, the delegate of the callback method, and the context object. When the call completes it will automatically call the callback that is supplied. At this point, the main thread waits until all the results are returned from the service.
Now let’s take a look at the callback. With an asynchronous client, when a calculation is completed by the service, there must be a means of communicating this status back to the client. The callback (or response handler) is implemented for this purpose. It is called by the Middleware each time the service completes a calculation. In the following sample, SimpleInterestCompleted() is the callback method. It accepts the result object, which is passed to the method by the middleware whenever the respective calculation has completed.
private static void SimpleInterestCompleted(IAsyncResult result){SimpleInterestMethodContext ctx =(SimpleInterestMethodContext)result.AsyncState;BasicGridCalculator.BasicCalculator calculator = ctx.obj;double amount = ctx.amount;try{double interest = calculator.EndSimpleInterest(result);Console.WriteLine("Principal : ${0},\t Interest: ${1}",amount, interest);}catch (Exception ex){Console.WriteLine("Exception Occured inSimpleInterestCompleted Callback : {0}",ex.ToString());}finally{lock (resultLock){outstandingMethodResults--;if (outstandingMethodResults < 1){outstandingMethodsCompleted.Set();}}}}
First, we restore the original context for the calculation. Next, we store a reference to the proxy on our result object so that we can make the call to calculator.EndSimpleInterest() when the callback is triggered from another thread. The result is retrieved by passing the result reference as a token to the EndSimpleInterest() method.
We count down the number of results outstanding. When all the results have returned, we set a flag, which unblocks the main thread in the CalculateInParallelWithCallback() method.