NGrid
Open source grid computing

Programming with NGrid

This document describes how to write C# code that will leverage the NGrid framework. The examples presented in this document requires the NGrid.Core v0.5+ library to be compiled and run, but no actual grid of computers is required (everything can be executed locally). This document requires some understanding of the multithreading, serialization and remoting techniques.

Content

Foreword
Hello World, NGrid style
Grid objects and proxies
By reference or by value transmission
Multithreading
Synchronization
Tuning the execution with attributes

Foreword

Grid computing consists of distributing a computation amoung a set of computers without any apriori network topology. Since grid computing adds an additional software complexity, grid computing should be used only if the computation is too heavy to be performed on a single machine. NGrid aims to make to developpment of grid application safe and easy.

Many grid computing toolbox are available, but NGrid introduces two innovative concepts that are not found elsewhere. First a true multithread programming model with a unified garbage collected memory. Second a separation of the logical grid and physical grid.

The NGrid programming model mimics the .Net multithread programming model. For the developper who wants to distributes its code there is neither messages nor commucations to take care of, the grid is totally transparent. If we define a perfectly transparent framework as a framework where not a single client code line need to be changed in order to convert a multithread application into a grid application, then NGrid is not perfect but we getting very close.

Additionaly NGrid separates the logical and physical grid. The logical grid is the grid programming model abstraction. In practice, the logical grid is the set of interfaces in front of the client code. On the other side, the physical grid is the software that actually distributes the code execution amoung the machines of the grid. NGrid distinguishes those two levels because if the logical grid software needs to be as stable as possible (any change to the logical grid might break the client code), the physical grid software might evolve with the hardware. Additionaly, various contexts (trusted and reliable machines within a cluster; or untrusted and unreliable machines over the internet for voluntary computing) actually requires various physical grids.

Hello World, NGrid style

Lets starts a new console project for the VS IDE. Adds the NGrid.Core.dll assembly in the references and compile the following code a HelloWorld.exe application.

using System;
using NGrid;

namespace HelloWorld
{
    [Serializable]
    public class GMyObject : GObject
    {
        public string CallMe()
        {
            return "Hello World";
        }
    }

    public class HelloWorld
    {
        public static void Main(string[] args)
        {
            GMyObject obj = (GMyObject) (new GMyObject()).GetProxy();
            Console.WriteLine(obj.CallMe());
        }
    }
}

Once compiled, this code can be directly run with the usual command-line

$ HelloWorld.exe
Hello World

We have just run our first NGrid program. The physical grid used is NGrid.Zero, a default grid where everthing is executed locally. Other physical grids are available, see our Running through NGrid.Loader page.

Grid objects and proxies

NGrid.Core proposes a transparent multi-threaded programming model. But in reality the objects and the the threads are (possibly) distributed to a set of machines. In order to provide a unified view of the memory, NGrid.Core provides a particular type called GObject that must be inherited for all the objects that live in the grid.

[Serializable]
public class GMyObj : GObject
{
    public void Foo() 
    {
        Console.WriteLine("Foo called.");
    }
}

The type GObject has several methods and properties. The GObject.GetProxy() method is especially important. The typical use of a GObject will follow the pattern here below

GMyObj obj = (GMyObj) (new GMyObj()).GetProxy();
obj.Foo();  // call made on a transparent proxy

The grid object is immediately proxied after instanciation. Now obj does not refer to the real object but only to a transparent proxy. The real object could have been moved anywhere on the grid but the exact location has no importance for the client code. The physical grid handles enterely the burden of forwarding any call to obj.Foo() to the real object whereever the real object lies. We will say that the object lives on the grid. In order to move freely between the machine of the grid, the GMyObj instance is serialized; therefore it is necessary to declare as serializable any GObject.

NGrid is a garbage collected programming model. Therefore no particular cleaning is required to "get rid" of the grid objects. When there is no more proxy that refers to a particular GObject instance, the GObject instance is simply garbage collected. The grid garbage collector behaves like the local garbage collector and does not follow any particular deterministic pattern.

Remember also that .Net transparent proxies cannot handle field call, therefore make sure not to make any call on fields outside the current object (if xyz is an accessible field calling this.xyz is ok, but bar.xyz will fail).

In short, grid objects

  • inherit GObject.
  • need to be serializable.
  • are garbage collected.
  • does not make "external" field call.

By reference or by value variable transmission

We have seen here below how to produce a grid object. The question that we will answer in this section is: what difference does it make if the object is living on the grid on not? The answer lies on the implied variable transmission method when a method call occurs between GObjects. Somehow a transmission by reference is ensured for all proxied GObjects whereas transmission by value occurs for all other objects (including non-proxied GObjects). Let us illustrate this with a small example. First let's consider the following classes.

[Serializable]
public class A
{
    int x;

    public A(int x) 
    { 
        this.x = x; 
    }

    public int Value
    {
        get { return x; }
        set { x = value; }
    }
}

[Serializable]
public class G : GObject
{
    int x;

    public G(int x) 
    { 
        this.x = x; 
    }

    public int Value
    {
        get { return x; }
        set { x = value; }
    }
}

The classes A and G are identical except for the fact that G inherits GObject. Let us now consider the behavior of the following GetA and GetG methods.

[Serializable]
public class GMyObj : GObject
{
    A a = new A(13);
    G g = (G) (new G(13)).GetProxy();

    public A GetA()
    {
        return a; // possible transmission by value
    }

    public G GetG()
    {
        return g; // guaranteed transmission by reference
    }
}

The behavior of GetG is the most simple is describe. Remember that after the call the GObject.GetProxy(), the variable g contains only a transparent proxy; and NGrid ensures that the value returned by GetG is transmitted by reference.

The behavior of GetA is a bit more complex because it depends of the origin of the call. If GetA is called from an other GObject living on a different machine of the grid, then the return value of GetA is going to be serialized/deserialized and therefore transmitted by value. If the call is originary from the GMyObj itself (ex: an other method calls this.GetA()) or from a GObject hosted on the same machine than the current instance of GMyObj, then the returned value might instead be lazily returned by reference.

Multithreading

The namespace NGrid.Threading mimics the System.Threading namespace. Let see a small example demonstrating this similarity.

using System;
using System.Threading;

using NGrid;
using NGrid.Threading;

[Serializable]
public class G : GObject
{
    public void Foo()
    {
        Console.WriteLine("Foo called.");
    }
}

public class MainClass
{
    public static void Main(string[] args)
    {
        G g = (G) (new G()).GetProxy();
        GThread gt = (new GThread(new ThreadStart(g.Foo))).GetProxy();

        gt.Start();
        gt.Join();
    }
}

As we have seen in the previous section, grid objects (note that GThread inherites GObject) are immediatly proxied by a call to GObject.GetProxy(). Then the methods Start() and Join() are called on the GThread object. Note that the ThreadStart delegate references a proxied grid object. any attempt to refers to a non-proxied grid object will fail.

As expected the Start and Join methods have the same semantic that their equalents in System.Threading.Thread. In the case of the mock grid, NGrid brings nothing but a small overhead to the multithread execution, but in case of real physical grid, if several machines are available, the code execution will be balanced on the machine of the grid.

Synchronization

Since the concurrent execution is based on GThreads that could be running on different grid machine, the C# synchronization primitive like the lock keyword or the System.Threading.Monitor class will not ensure grid level synchonization. In order to ensure grid level synchronization, NGrid provides the NGrid.Threading.GMonitor class that have the same semantic than the Monitor class. Naturally, grid synchronization could only be performed on grid objects. The following code illustrates the use of the NGrid.Lock class.

[Serializable]
public class G : GObject
{
    GObject gridLock = new GObject();

    public void SyncFoo()
    {
        using(new Lock(gridLock))
        {
            // grid sync block
        }
    }
}

The Lock class is a simple syntatic sugar calling respectively GMonitor.Enter and GMonitor.Exit when instanciated and disposed.

Tuning the execution with attributes

Optimizing the execution over a grid of a multithreaded code is a very complex task. One might wonder if it is even feasible. In order to simplify the life of physical grid implementers, NGrid.Core includes some execution tuning attributes that could be used to decorate the client code. The most important characteristic of those tuning attributes is their impactlessness on the semantic of the client code. In other words, with or without those attributes, a given code produce the same output, only the execution time may vary.

The unified memory view, e.g. one GObject can reference any other GObject, is very convenient for the client developper. Unfortunately, it can be the cause of a lot of very slow remote calls since the GObjects are distributed on the various machines of the grid. Not handled carefully, those remote calls are the bottleneck of the grid computation. One simple and intuitive solution is the use of local grid object clones, also called in the following replicates. A replicate is a local copy of a distant grid object. If such replicate is available, a method call can be performed locally instead of having to goes through the whole grid.

But what happen if the call modifies the replicate itself? Should all the replicates be updated? Additionally, simply checking if an object has been modified or not could be a costly computation. Instead NGrid.Core provides attributes that can be used by the client developper to specify the behavior that the physical grid should adopt with the grid objects. Let's see an introducing example.

[Serializable]
public class G : GObject
{
    int x;
    int y;

    public G(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    [ReplicateSafe] 
    public int GetX()
    {
        return x;
    }

    public int GetY()
    {
        return y++;
    }
}

Take a look at the ReplicateSafe attribute. When applied to a method (or a property), the ReplicateSafe attribute indicates that it is authorized to execute the method call on a local replicate rather than on the real object. It does imply that the instance will be replicated, it just give the permission to the physical grid to perform such replication. The developer must ensures that any method tagged with the ReplicateSafe attribute has no side-effect on the GObject. By default methods or properties are not considered as replicate-safe. In the example here above, GetX has no side effect and is marked as replicate-safe. On the other hand, GetY modifies the GObject instance and should not be marked as replicate-safe.