Wednesday, 11 November 2009

Create an Intelligent Thread-Safe Queued BackgroundWorker

Problem:

If you run the RunWorkerAsync method of normal BackgroundWorker class when a previous RunWorkerAsync has not yet been completed, then you will get an exception. What this means is that before calling the RunWorkerAsync again, you must be sure that the Completed event of the previous call has been reached.

Solution:

Features of my QueuedBackgroundWorker:

  • Run different methods asynchronously and get the result back if you would want to.
  • All the works will be queued and will run one by one.
  • You can have complex input or output arguments.
  • It’s thread-safe

QueuedBackgroundWorker:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;

/// <summary>
/// This is thread-safe
/// </summary>
public class QueuedBackgroundWorker
{
#region Constructors

public QueuedBackgroundWorker() { }

#endregion

#region Properties

Queue<object> Queue = new Queue<object>();

object lockingObject1 = new object();

public delegate void WorkerCompletedDelegate<K>(K result, Exception error);

#endregion

#region Methods

/// <summary>
/// doWork is a method with one argument
/// </summary>
/// <typeparam name="T">is the type of the input parameter</typeparam>
/// <typeparam name="K">is the type of the output result</typeparam>
/// <param name="inputArgument"></param>
/// <param name="doWork"></param>
/// <param name="workerCompleted"></param>
public void RunAsync<T,K>(Func<T, K> doWork, T inputArgument, WorkerCompletedDelegate<K> workerCompleted)
{
BackgroundWorker bw = GetBackgroundWorker<T,K>(doWork, workerCompleted);

Queue.Enqueue(new QueueItem(bw, inputArgument));

lock (lockingObject1)
{
if (Queue.Count == 1)
{
((QueueItem)this.Queue.Peek()).RunWorkerAsync();
}
}
}

/// <summary>
/// Use this method if you don't need to handle when the worker is completed
/// </summary>
/// <param name="doWork"></param>
/// <param name="inputArgument"></param>
public void RunAsync<T,K>(Func<T, K> doWork, T inputArgument)
{
RunAsync(doWork, inputArgument, null);
}

private BackgroundWorker GetBackgroundWorker<T, K>(Func<T, K> doWork, WorkerCompletedDelegate<K> workerCompleted)
{
BackgroundWorker bw = new BackgroundWorker();
bw.WorkerReportsProgress = false;
bw.WorkerSupportsCancellation = false;

bw.DoWork += (sender, args) =>
{
if (doWork != null)
{
args.Result = (K)doWork((T)args.Argument);
}
};

bw.RunWorkerCompleted += (sender, args) =>
{
if (workerCompleted != null)
{
workerCompleted((K)args.Result, args.Error);
}
Queue.Dequeue();
lock (lockingObject1)
{
if (Queue.Count > 0)
{
((QueueItem)this.Queue.Peek()).RunWorkerAsync();
}
}
};
return bw;
}

#endregion
}

public class QueueItem
{
#region Constructors

public QueueItem(BackgroundWorker backgroundWorker, object argument)
{
this.BackgroundWorker = backgroundWorker;
this.Argument = argument;
}

#endregion

#region Properties

public object Argument { get; private set; }

public BackgroundWorker BackgroundWorker { get; private set; }

#endregion

#region Methods

public void RunWorkerAsync()
{
this.BackgroundWorker.RunWorkerAsync(this.Argument);
}

#endregion
}

1) How to use it:

public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
var worker = new QueuedBackgroundWorker();

worker.RunAsync<int, int>(Calculate, 2);

worker.RunAsync<MultiplyArgument, int>(Multiply, new MultiplyArgument() { A = 1, B = 2 }, MultiplyCompleted);
}

private int Calculate(int a)
{
return a*2;
}

private int Multiply(MultiplyArgument calculateArgument)
{
return calculateArgument.A * calculateArgument.B;
}

private void MultiplyCompleted(int result, Exception error)
{
Response.Write("worker completed, result: " + result.ToString());
}

}

public class MultiplyArgument
{
public int A { get; set; }

public int B { get; set; }

}

7 comments:

Tag said...

it is working very good and solved my problem thanks a lot man

Tag said...
This comment has been removed by the author.
Fer_Show said...

I just want to say thanks for this, it helped me a lot, really. People like you deserves heaven, hehehe. really appreciate it, thanks again

Snark said...

How would you stop it .. say you've got all your queued work going then you need to stop for any reason .. not to resume .. but pausing would also be good .

Bhupinder Singh said...

What if I have a function with 3 arguements that needs to be queued using this class? How do I write the expression?

Say the function is called normally as playAudiofile(thisCounter.counter_prefix, thisCounter.counter_name, thisCounter.token_number);

And I am using backgroundworker1 control. Should i remove that?

Bhupinder Singh said...

Okay i got that working. i would like to know that if there are many timers on the application, then would the thread sync be affected? Because Im trying to play a series of wav files via wmplib and it happens on button click.

so if i click the button 3 times, the 2nd thread and 3rd thread overlap with the 1st one. Why is that happening?

Hans De Mulder said...

Also, implement IDisposable, as the BackgroundWorker implements the IDisposable interface.