Thursday, June 2, 2016

Task Queue Revisited

Several years back, I wrote about  A Simple Task Queue and made a sort of promise that if I got time, I would enhance it a bit.

Here is my enhanced version.

    public class TaskQueue
    {
        public delegate void TaskDelegate();
        public TaskDelegate TaskDlgt { get; private set; }

        public TaskQueue(TaskDelegate dlgt) : this(dlgt, null) { }
        public TaskQueue(TaskDelegate dlgt, Queue<TaskDelegate> q)
        {
            _Q = q;
            TaskDlgt = dlgt;
        }

        public void Enqueue()
        {
            lock(_Q)
            {
                if(Busy)
                    _Q.Enqueue(TaskDlgt);
                else {
                    Busy = true;
                    TaskDlgt.BeginInvoke(TaskCallback, TaskDlgt);
                }
            }
        }
        private void NextTask()
        {
            TaskDelegate dlgt;
            lock (_Q)
            {
                if (_Q.Count > 0)
                {
                    dlgt = _Q.Dequeue();
                    dlgt.BeginInvoke(TaskCallback, dlgt);
                }
                else
                    Busy = false;
            }
        }
        private void TaskCallback(IAsyncResult ar)
        {
            TaskDelegate dlgt = ar.AsyncState as TaskDelegate;
            if (dlgt.Equals(TaskDlgt))
                dlgt.EndInvoke(ar);
            NextTask();
        }

        private Queue<TaskDelegate> _Q { get { return _q ?? _staticQ; } set { _q = value; } }
        private Queue<TaskDelegate> _q;
        private static Queue<TaskDelegate> _staticQ = new Queue<TaskDelegate>();
        private static Dictionary<Queue<TaskDelegate>, bool> BusyDict = new Dictionary<Queue<TaskDelegate>, bool>();
        private bool Busy { get { if (!BusyDict.ContainsKey(_Q)) BusyDict.Add(_Q, false); return BusyDict[_Q]; } set { if (!BusyDict.ContainsKey(_Q)) BusyDict.Add(_Q, false); BusyDict[_Q] = value; } }
    }

This Task Queue, unlike the earlier one, does not require that the developer inherit from an abstract class.  This Task Queue also allows for multiple user supplied queues.

To use it, create an instance of the class passing it a delegate, for example:

        var task = new TaskQueue(() => Console.WriteLine("This is a test") );
        task.Enqueue();

This Task Queue follows a "Set and Forget" pattern.  It is useful when you simply want to get things of limited duration done in the background without having to block the main flow of logic.  Obviously, your task must eventually finish, else the other tasks on the queue will block indefinitely.  Using the above example, the same task could be resubmitted as often as needed.  For example:

        var task = new TaskQueue(() => Console.WriteLine("This is a test"));
        for (int i = 0; i < 5; ++i)
            task.Enqueue();

You can also supply your own queue or queues to override the default queue. This offers a means for managing the tasks. For example:

        var myQueue = new Queue<TaskQueue.TaskDelegate>();
        new TaskQueue(() => ScrapeWebPage("https://yacsharpblog.blogspot.com"), myQueue).Enqueue();
        new TaskQueue(() => Highlight("yacsharp"), myQueue).Enqueue();

To manage the tasks, place a lock on the queue you supply and then adjust the queue however you like. An obvious management operation is to cancel all tasks. That can be done by locking and emptying the queue (as long as no other task continues to add to the queue).

        if(myQueue != null)
        {
            lock(myQueue)
            {
                myQueue.Clear();
            }
        }

Even though the TaskDelegate is a void function taking no arguments, complex situations can be handled with anonymous functions and providing a state object to each task for communicating with other tasks or back to the originator.

One area of improvement that I leave for an exercise is that of handling exceptions. The IAsyncResult object encapsulates any uncaught exceptions thrown by the task and raises them again when the EndInvoke method is called. The simplest approach would be to ignore the uncaught exception and continue which is consistent with a Set and Forget pattern.

        private void TaskCallback(IAsyncResult ar)
        {
            try
            {
                TaskDelegate dlgt = ar.AsyncState as TaskDelegate;
                if (dlgt.Equals(TaskDlgt))
                    dlgt.EndInvoke(ar);
            }
            catch
            {
            }
            NextTask();
        }



But other approaches are available. See what you can do with it.