Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / threads

Simple Cooperative Task Scheduling in C#

4.63/5 (4 votes)
19 Nov 2012CPOL3 min read 20.4K   3  
A sample WPF application that shows simple background tasks running on the UI thread.

Introduction

Let's face it, writing code that executes in the background is difficult. You can either write threaded code and marshal objects between your threads, or you can write state machines which are really unreadable. It would be nice if you could just write a method and somehow invoke it to run in the background without worrying about marshaling objects. It turns out that with a bit of trickery you can pretty much do just this...

Using the code

What we would like to do, is write a method like the following and have it run without blocking our UI.

C#
private long CalculateFibonacci(long n)
{
    long a = 0;
    long b = 1;
 
    // In N steps compute Fibonacci sequence iteratively.
    for (long i = 0; i < n; i++)
    {
        long temp = a;
        a = b;
        b = temp + b;
    }
 
    // Let the client know the result.
    return a;
}

To do this we need to define points in the code where it will cooperatively give up control and allow other tasks to execute. This will allow the UI to update, etc. What we need is a "yield return" statement.

As many of the readers of this article will know, C# has just such a statement. The drawback to the statement is that it can only be used when defining an enumerator. So what is the difference between our method and an enumerator? There doesn't need to be all that much of a difference. If we imagine that our method is executing iterative cycles to generate the result we are looking for, then we could insert yields at the end of each iteration to return control from the method, but without losing the state of the method.

So what if our code above looked like this:

C#
private IEnumerable<double> CalculateFibonacci(long n, Action<long> setResult)
{
    long a = 0;
    long b = 1;
 
    // In N steps compute Fibonacci sequence iteratively.
    for (long i = 0; i < n; i++)
    {
        long temp = a;
        a = b;
        b = temp + b;
 
        yield return (double)i / (n - 1);
    }
 
    // Call the callback delegate to let the client know the result.
    setResult(a);
}

It's still pretty readable, right? No complicated state machines or thread marshaling, right?

The only differences are that we are now returning the progress of the method from the yield return and the actual return requires a delegate to communicate its value to the caller.

Now we have the basics of cooperative multitasking, but it would probably be nice to have a central place that calls and manages this logic.

For that I created the WorkPool class:

C#
public class WorkPool
{
    private ObservableCollection<worktask> workers = new ObservableCollection<worktask>();
    private int index;
 
    public IEnumerable<worktask> Workers
    {
        get
        {
            return this.workers;
        }
    }
 
    public void Add(string name, IEnumerator<double> worker)
    {
        this.workers.Add(new WorkTask(){ Name = name, Enumerator = worker, Progress = 0.0});
    }
 
    public bool Run(TimeSpan timeout)
    {
        // Determine a time to quite out of our loop.
        DateTime runTill = DateTime.Now + timeout;
 
        while (DateTime.Now < runTill)
        {
            if (workers.Count == 0)
            {
                // If all the work is done then get out of the loop.
                break;
            }
            else if (this.index >= workers.Count)
            {
                // If we fall of the end of the work task list, start at the beginning.
                this.index = 0;
            }
 
            // Move to the next iteration of the task.
            IEnumerator<double> next = workers[this.index].Enumerator;
            var hasMore = next.MoveNext();
 
            // Save the progress for easy access.
            workers[this.index].Progress = workers[this.index].Enumerator.Current;
 
            if (!hasMore)
            {
                // If the task is done, remove it.
                workers.RemoveAt(this.index);
            }
            else
            {
                // Move to the next task.
                this.index++;
            }
        }
            
        // Return true if there is more work to do.
        return workers.Count > 0;
    }
}

The WorkPool class has an Add method for adding new tasks and a Run method that when called will execute any queued task and quit after the timeout value is reached. Pretty neat, right?

So let's wrap this up into an application. Below is a snippet from my WPF test project.

C#
private void Button_Click(object sender, RoutedEventArgs e)
{
    // Create the task.
    var value = long.Parse(this.num.Text);
    var i = CalculateFibonacci(value, this.ShowResult);
 
    // Add the work task to the work task pool.
    this.pool.Add(string.Format("Fibonacci {0}", value), i.GetEnumerator());
 
    // Queue the idle method to do all the executing of the task.
    this.Dispatcher.Invoke(new Action(this.ApplicationIdle), DispatcherPriority.ApplicationIdle, null);
}
 
private void ApplicationIdle()
{
    // Update the work tasks.
    if (this.pool.Run(new TimeSpan(0, 0, 0, 0, 50)))
    {
        // Requeue the Idle method if needed.
        this.Dispatcher.Invoke(new Action(this.ApplicationIdle), 
             DispatcherPriority.ApplicationIdle, null);
    }
}

Here I have a button click event that creates our task, queues it in the work pool, and then invokes a delegate that runs in application idle to execute the task. The idle method executes the pool for 50 milliseconds, then if it needs to execute some more, it requeues itself to run in application idle again.

Conclusion

The attached code shows a WPF application that calculates Fibonacci numbers in idle time on the UI thread using the method described above. The code is, I believe, very readable, maintainable, and robust. Obviously on a multi-core machine, you can achieve more by moving away from a single threaded design. I have however seen very complicated applications running in single threads and this is a neat way of keeping your code manageable.

History

  • 8/22/2012 - Initial submission.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)