Vorbb 'cuz its fresh!


Post Reply 
 
Thread Rating:
  • 0 Votes - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Tutorial - C# - Multi Threaded Applications
Author Message
Burningmace
admin
Administrators

Posts: 336
Joined: May 2009
Reputation: 1
Vorbb Bux: 9171

Thanks: 1
48 thank was given in 19 posts
Post: #1
Tutorial - C# - Multi Threaded Applications
Since the recent launch of multi-core processors as the standard for desktop and laptop machines there has been a lot of talk about multi-threading. Threading facilitates concurrent execution of code in an application, allowing for asynchronous operation within programs. However, as with all technologies there are all sorts of things that need to be taken into consideration.

In C#, we can create a thread like this:
Code:
using System.Threading;

public class ThreadTestApp
{
    public static void Main()
    {
        ThreadTest tt = new ThreadTest();
        Thread t = new Thread(new ThreadStart(tt.DoWork));
        t.Start();
    }
}

public class ThreadTest
{
    private void DoWork()
    {
        // Put your thread code here
    }
}

We can also create threads with parameters:
Code:
using System.Threading;

public class ThreadTestApp
{
    public static void Main()
    {
        ThreadTest tt = new ThreadTest();
        String myParam = "Hello";
        Object param = (Object)myParam;
        Thread t = new Thread(new ParameterizedThreadStart(tt.DoWork, param));
        t.Start();
    }
}

public class ThreadTest
{
    private void DoWork(Object param)
    {
        // Put your thread code here
    }
}

If you need to pass more than one parameter to a thread create a structure and give it all the members you need, then pass it as your parameter. Inside the thread, declare an object with the struct as the type and cast the parameter.
Code:
private void DoWork(Object param)
{
    MyStruct p = (MyStruct) param;
    // Put your thread code here
}

All this seems simple enough, but if you have two or more threads writing to the same variable, you start to run into problems. The following is an example of what can happen when two threads try to write to the same variable at the same time:

1)Load Value into Thread 1 Register
2)Load Value into Thread 2 Register
3)Perform operations on Thread 1 Register
4)Perform operations on Thread 2 Register
5)Save Thread 1 Register to Value
6)Save Thread 2 Register to Value

As you can see, the result of anything done by thread 1 is overwritten in step 6. For example, if Value starts off at 50 and both threads increment the value by 1, the result will be that Value is 51, not 52. To remedy this, Microsoft provide us with the Interlocked class. The three most useful methods in Interlocked are Increment, Decrement and Exchange. As they imply, Increment and Decrement add or subtract 1 from a variable.
Code:
using System.Threading;

public class ThreadTestApp
{
    public static void Main()
    {
        ThreadTest tt = new ThreadTest();
        Thread t1 = new Thread(new ThreadStart(tt.Add));
        Thread t2 = new Thread(new ThreadStart(tt.Subtract));
        t1.Start();
        t2.Start();
    }
}

public class ThreadTest
{
    private int MyValue = 100;

    public void Add()
    {
        for(int n = 0; n < 5000; n++)
        {
            Interlocked.Increment(ref MyValue);
        }
    }
    
    public void Subtract()
    {
        for(int n = 0; n < 5000; n++)
        {
            Interlocked.Decrement(ref MyValue);
        }
    }
}

Interlocked operations are atomic – i.e. no two operations can occur at the same time. This means that operations cannot overlap and therefore the above problem is avoided. Exchange works in the same way, but instead sets a variable's value and returns the old value:
Code:
Object Interlocked.Exchange(ref Object, Object);
Obviously none of this helps when using object write methods such as the List object's Add method. There are two ways to handle such code. The first is called a lock statement and is implemented in C# using the 'lock' keyword.
Code:
public void DoWork
{
    String str = "my string";
    lock
    {
        MyList.Add(str);
    }
}

The lock keyword is useful for handling multiple lines of code that must not be asynchronously executed. Just like with Interlocked, any code within a lock cannot run at the same time as any other code in a lock when both blocks access the same objects.
Code:
public void WorkThread1
{
    String str = "my string";
    lock
    {
        StringList.Add(str);
        IntegerList.Add(5);
    }
}

public void WorkThread2
{
    String str = "something";
    lock
    {
        StringList.Add(str);
    }
}

public void WorkThread3
{
    lock
    {
        IntegerList.Add(3);
    }
}

The lock within WorkThread1 prevents either block of locked code in WorkThread2 or WorkThread3 from being executed at the same time as it. However, WorkThread2 and WorkThread3's blocks of locked code are free to run at the same time as each other because they do not share any objects. The compiler detects which objects are in which locks and handles how the threads are executed. A lock allows a set of code to run in sequence without another interjecting. The downside of this method is that it is very slow.

The other way to deal with this problem is using a principle called Mutexes. They work in exactly the same way as the lock statement, but they work on single objects only. Of course, you can create a whole bunch of mutexes to keep your object writes in check.
Code:
private Mutex mut = new Mutex();

public void WorkThread
{
    mut.WaitOne();
    // Do work here
    mut.ReleaseMutex();
}

The upside of this is that it is fast. If you've got several threads of the same method running at once you're best off using mutexes as they allow more flexible write access to objects. Something to be avoided is nesting and overlapping:
Code:
private Mutex il_mutex = new Mutex();
private Mutex sl_mutex = new Mutex();

public void WorkThread
{
    il_mutex.WaitOne();
    // Do work
    sl_mutex.WaitOne();
    // More Work
    il_mutex.ReleaseMutex();
    // Even more work
    sl_mutex.ReleaseMutex();
}

The problem with this kind of code is that you can create something called a race condition. This is where the application enters a sort of "catch 22", in which two threads are waiting on each other for mutexes to be released:

1)Thread 1 enters mutex A.
2)Thread 2 enters mutex B.
3)Thread 2 tries to enter mutex A, but must wait for Thread 1
4)Thread 1 tries to enter mutex B, but must wait for Thread 2

As you can see, both threads are now stuck waiting for each other. This freezes the threads completely. You can remedy this by adding a wait to the call:
Code:
private Mutex il_mutex = new Mutex();
private Mutex sl_mutex = new Mutex();

public void WorkThread
{
    Random rng = new Random();
    // The parameter of WaitOne specifies how long it should wait for mutex acquisition
    // Choosing a random time helps stop race conditions
    while(!il_mutex.WaitOne(rng.Next(5, 20)))
    { }
    while(!sl_mutex.WaitOne(rng.Next(5, 20)))
    { }
    il_mutex.ReleaseMutex();
    sl_mutex.ReleaseMutex();
}

Some objects are a problem because of performance, not cross-thread writes. For example, having 500 threads write to a file stream at the same time can cause some serious performance issues. A solution to this is the Semaphore class.
Code:
// Create a semaphore that allows up to 5 concurrent accesses to an object.
// The first parameter is the number of initially available concurrent requests
// The second parameter is the maximum number of available concurrent requests
Semaphore pool = new Semaphore(5, 5);
FileStream fs = File.OpenRead("C:\output.dat");

public void WorkThread
{
    // Do work here, save result in byte array called data

    // WaitOne uses up one available slot in the semaphore
    pool.WaitOne();
    fs.Write(data, 0, data.Length);
    fs.Flush();
    // Release makes one more slot available
    pool.Release();
}

The next problem is to do with something called context. In the same way that a private scope object cannot be accessed from outside a class, an object in one thread's context cannot be directly written to from another's context. We can use a delegate method in the same way that you would expose a private variable using a public property or method.
Code:
using System.Threading;

public class ThreadTestApp
{
    public delegate void UpdateLabelDelegate(Label toUpdate, String value);

    public static void Main()
    {
        // This form should have a label called lblProgress
        ThreadTest tt = new ThreadTest();
        Thread t1 = new Thread(new ThreadStart(tt.DoWork));
        t1.Start();
    }

    public void UpdateLabel(Label toUpdate, String value)
    {
        toUpdate.Text = value;
    }
}

public class ThreadTest
{
    private void DoWork()
    {
        for(int n = 0; n < 5000; n++)
        {
            ThreadTestApp.Invoke(new ThreadTestApp.UpdateLabelDelegate(UpdateLabel),
                            ThreadTestApp.lblProgress, n.ToString());
        }
    }
}

The above code invokes the UpdateLabel method on the form's thread context using the UpdateLabelDelegate delegate method. All writes to controls on forms must be done on the form's own context using delegates. The debugger throws an InvalidOperationException in the IDE if you don't use delegates, but will ignore the exception in a build environment. This does not mean that we can just ignore it, as the application will not work properly if concurrent writes mess up our code. You should always safely handle object access in applications with multiple threads.

My personal suggestion is to debug any production applications on both single-core and multi-core processor machines. So now you know how to use threads, you can throw them around copiously in your applications.

[Image: 29fxcgo.jpg]
Professional web design
"Why hello there Sir, might I perhaps take part in eating your brains?"
06-18-2009 07:13 AM
Visit this user's website Find all posts by this user Quote this message in a reply
Sponsors
Post Reply 


Forum Jump:


Contact Us - Vorbb - Return to Top - Return to Content - Lite (Archive) Mode - RSS Syndication