Wednesday, February 11, 2009

Refactoring single threaded code to multithreaded and to Tasks (with Parallel Extension Framework)

Source Code

Introduction

Someone asked me to post a simple sample on how to build a multi threaded application. So I put together this blog post. Of course to maintain everything readable and easy to follow there’s no error handling and no validation.

Starting point

A-SingleThreadSimpleModel in the source code

For this sample I used a person as my model object. Someone could say that I should not use stupid objects but its more understandable this way.

So we have a person that could talk and walk. Obviously most of us can do these to things at the same time, but we must tell the computer how to do it. First we will start by a single threaded model to show you all the objects involve. Then we will refactor it a little to make it more manageable and then we will make it multi treaded using first thread an later tasks. Task is a new concept that will be introduced in Visual Studio 2010.

Let’s look at the Person class:

public class Person
{
    private int _position;
    private readonly List<string> _log = new List<string>();

    public void Say(string message)
    {
        var words = message.Split(' ');
        foreach (var word in words)
        {
            Thread.Sleep(400);
            SomethingSaid(this, new TalkEventArgs(word));
        }
        Log.Add(message); // not thread safe
    }

    public void Walk(int steps)
    {
        for (int i = 0; i < steps; i++)
        {
            Thread.Sleep(100);
            _position++; // not thread safe
            MovedForward(this, new WalkEventArgs(1));
        }
    }

    public int Position { get { return _position; } }

    public List<string> Log { get { return _log; } }

    public event EventHandler<TalkEventArgs> SomethingSaid = delegate { };
    public event EventHandler<WalkEventArgs> MovedForward = delegate { };
}

We have a very simple class here with two properties, two methods and two events. After initialising this class we can call Say(…) or Walk(…) like demonstrated in this console application:

public class Program
{
    static void Main(string[] args)
    {
        var p = new Person();
        p.SomethingSaid += (sender, e) => Console.WriteLine("I'm saying: {0}", e.Message);
        p.MovedForward += (sender, e) => Console.WriteLine("I walked: {0} steps", e.Steps);

        Stopwatch stopwatch = Stopwatch.StartNew();
        p.Walk(20);
        p.Say("Let me tell you something");

        stopwatch.Stop();
        Console.WriteLine("It took me {0} ms\nto walk {1} steps\nand say \"{2}\"",
            stopwatch.ElapsedMilliseconds,
            p.Position,
            String.Join("; ", p.Log.ToArray()));
    }
}

If you run this sample you should get something like this:

I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I'm saying: Let
I'm saying: me
I'm saying: tell
I'm saying: you
I'm saying: something
It took me 5511 ms
to walk 20 steps
and say "Let me tell you something"
Press any key to continue . . .

We can clealry see that only one task ca be done at any time. We either walk or talk but not both.

Refactoring

B-SingleThreadRefactored in the source code

Now let’s take this sample to the next level. In this refactoring phase we will:

  • Extract an interface
  • Build a base class
  • Add a little thread safety

First the interface:

public interface IPerson
{
    void Say(string message);
    void Walk(int steps);

    int Position { get; }

    IEnumerable<string> Log { get; }

    event EventHandler<TalkEventArgs> SomethingSaid;
    event EventHandler<WalkEventArgs> MovedForward;
}

This interface will then be implemented by a base class:

public abstract class PersonBase : IPerson
{
    private int _position;
    private readonly List<string> _log = new List<string>();

    #region Implementation of IPerson

    public abstract void Say(string message);

    public abstract void Walk(int steps);

    public int Position { get { return _position; } }

    public IEnumerable<string> Log { get { return _log; } }

    public event EventHandler<TalkEventArgs> SomethingSaid = delegate { };
    public event EventHandler<WalkEventArgs> MovedForward = delegate { };

    #endregion

    protected void Moving(int steps)
    {
        for (int i = 0; i < steps; i++)
        {
            Thread.Sleep(100);
            Interlocked.Increment(ref _position); // thread safe
            MovedForward(this, new WalkEventArgs(1));
        }
    }

    protected void Speaking(string message)
    {
        var words = message.Split(' ');
        foreach (var word in words)
        {
            Thread.Sleep(400);
            SomethingSaid(this, new TalkEventArgs(word));
        }
        _log.Add(message); // not thread safe yet!
    }
}

Executing the code after this refactoring should gives you the exact same output as before.

Using Thread

C-MultiThreadUsingThread in the source code

Now let’s try to multi thread this sample a little. We will define a new interface for that. This interface will declare 4 new members: BeginSay, EndSay, BeginWalk and EndWalk. All the “begin” method will initiate background thread to do the task. All the “end” method will act as synchronization mechanism. This is where we can catch exception. Here is the IThreadPerson interface:

public interface IThreadPerson : IPerson
{
    Thread BeginSay(string message);
    void EndSay(Thread callBack);
    Thread BeginWalk(int steps);
    void EndWalk(Thread callBack);
}

And an implementation of it:

public class Person : PersonBase, IThreadPerson
{
    #region IThreadPerson Members

    public Thread BeginSay(string message)
    {
        var starter = new ThreadStart(() =>
        {
            Say(message);
        });
        var thread = new Thread(starter);
        thread.Start();
        return thread;
    }

    public void EndSay(Thread talk)
    {
        talk.Join();
    }

    public Thread BeginWalk(int steps)
    {
        var starter = new ThreadStart(() =>
        {
            Walk(steps);
        });
        var thread = new Thread(starter);
        thread.Start();
        return thread;
    }

    public void EndWalk(Thread walk)
    {
        walk.Join();
    }

    #endregion
}

Each “begin” method will return a new thread which we can use to synchronize the process. By calling “end” method we explicitly wait for the result. These method can be placed into a try catch block to handle any possible exeption that may occur.

If you run this you will get something like this:

I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I'm saying: Let
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I'm saying: me
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I'm saying: tell
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I'm saying: you
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I walked: 1 steps
I'm saying: something
It took me 2017 ms
to walk 20 steps
and say "Let me tell you something"
Press any key to continue . . .

As you can see it took about half the tome to complete and we are doing two things at the same time.

Using Task

D-MultiThreadUsingTask in the source code

Using Task from Parallel Extensions Framework is almost the same as Thread except some types and the way to create a thread are different.

Here is the ITaskPerson interface:

public interface ITaskPerson : IPerson
{
    Task BeginSay(string message);
    void EndSay(Task callBack);
    Task BeginWalk(int steps);
    void EndWalk(Task callBack);
}

And the task implementation of Person:

public class Person : PersonBase, ITaskPerson
{
    #region ITaskPerson Members

    public Task BeginSay(string message)
    {
        var task = Task.Create(x => { Say(message); });
        return task;
    }

    public void EndSay(Task talk)
    {
        talk.Wait();
    }

    public Task BeginWalk(int steps)
    {
        var task = Task.Create(x => { Walk(steps); });
        return task;
    }

    public void EndWalk(Task walk)
    {
        walk.Wait();
    }

    #endregion
}

As you can see it’s a little simpler to create a Task than a Thread. The usage is also simpler. Here how to call it:

Task walk = p.BeginWalk(20);
Task talk = p.BeginSay("Let me tell you something");
p.EndWalk(walk);
p.EndSay(talk);

But this way of doing is somehow deprecated. Here is a more “Parallel Extensions” way:

Task[] tasks = new Task[]
{
    p.BeginWalk(20),
    p.BeginSay("Let me tell you something")
};
Task.WaitAll(tasks);

The execution of both method should give you almost the same result as with the Thread version.

Conclusion

I hope it will help you building multi thread and thread safe application.

No comments: