3

All, I have a generic method called TaskSpin, in this method I launch a Task with an ascociated continutation

public TaskSpin(Func asyncMethod, object[] methodParameters)
{
    ...
    asyncTask = Task.Factory.StartNew<bool>(() => 
        asyncMethod(uiScheduler, methodParameters));

    asyncTask.ContinueWith(task =>
    {
        // Finish the processing update UI etc.
    }
    ...
}

The problem is now that I want to run multiple methods using TaskSpin, but I need to restrict the methods to run one-at-a-time. So foreach row in some DataGridView I want to do something like

foreach (DataGridViewRow row in this.DataGridViewUrg.Rows)
    TaskSpin(Run(DrgDataRowInfo(row.Index)));

However, in the above the TaskSpin method will exit immediately causing TaskSpin to spin off the next method on yet another thread. This is no good as the Run method write to a common set of files. What is the best way to queue these jobs?

Cœur
  • 37,241
  • 25
  • 195
  • 267
MoonKnight
  • 23,214
  • 40
  • 145
  • 277

4 Answers4

2

You could implement your own task queue and just keep processing the queue after each task is complete until it's empty e.g.

using TaskPair = KeyValuePair<Func, object[]>;
...

private Queue<TaskPair> taskQueue;
...

// generate the queue of tasks
this.taskQueue = new Queue<TaskPair>(this.DataGridViewUrg.Rows);
foreach (DataGridViewRow row in this.DataGridViewUrg.Rows)
{
    var task = new TaskPair(Run(DrgDataRowInfo(row.Index)), /* params */);
    this.taskQueue.Enqueue(task);
}
// initiate queue processing
ProcessNextTask();

....
private void ProcessNextTask()
{
    try
    {
        var item = this.taskQueue.Dequeue();
        TaskSpin(item.Key, item.Value);
    }
    catch(InvalidOperationException)
    {
        // queue is empty
    }   
}

....
// Execute task and process next in queue (if applicable)
public TaskSpin(Func asyncMethod, object[] methodParameters)           
{            
    ...           
    asyncTask = Task.Factory.StartNew<bool>(() =>            
        asyncMethod(uiScheduler, methodParameters));           

    asyncTask.ContinueWith(task =>           
    {           
        // Finish the processing update UI etc.
        ProcessNextTask();           
    }  
    ...                 
}
James
  • 80,725
  • 18
  • 167
  • 237
  • Yeah, I like this alot. Cheers. I was going to use a `ConncurrentCollection` (`BlockinCollction`) can you comment on this approach? Thanks for your time. – MoonKnight Aug 01 '12 at 11:46
  • Downvoting this is ridiculous. FACT. – MoonKnight Aug 01 '12 at 11:48
  • 1
    @Killercam you could swap out `Queue` for `ConcurrentQueue` as you are still *technically* updating the queue from a different thread. However, I think you should be ok considering you aren't simultaenously modifying it. – James Aug 01 '12 at 11:50
  • Just a question: Why not just use a continuation chain like I suggested in my answer? It's miles simpler. The only downfall is that you won't have a reference to all the queued tasks... but is that necessary? Thanks. – Dave New Aug 01 '12 at 11:55
  • Because this involves modifying a complex method (TaskSpin) that no only controls this process but others aswell... Thanks for your help. – MoonKnight Aug 01 '12 at 12:03
  • @James I have implemented something like the above. It works well but when I call the `ProcessNextTask()` from the continuation the UI freezes. I have tried to use a Task.Factory.StartNew(() = > ... to launch the ProcessNextTask not on the UI thread but this has not helped. Can you advise? – MoonKnight Aug 01 '12 at 14:55
  • @Killercam You should be wrapping your all of your UI updating code (including `ProcessNextTask`) and invoking it on the main thread. Are you doing that at the moment? – James Aug 01 '12 at 15:17
  • I use `TaskScheduler`s to invoke the required operations on the UI thread. This works well and seems to be the correct way when using the TPL... I have asked another question using the method you have provided [here](http://stackoverflow.com/questions/11762551/using-a-task-continuation-to-stack-jobs). This problem is an odd one and I am not sure what is going on as yet. Any help is appreciated, thanks for your time... – MoonKnight Aug 01 '12 at 15:31
  • @Killercam had a look at your other question, not sure you are familiar with how aliases work in Visual Studio. `TaskPair` in my example above is just an alias I created for `KeyValuePair`. That code goes up where you declare your namespaces and then instead of repeating the `new KeyValuePair(..)` everywhere you just do `new TaskPair(...)`. – James Aug 02 '12 at 07:59
  • @James Thanks, I have used aliases before but I have to play by the rules of the code base for this particular application. In this particular code, aliases are not permitted. I am not sure why (seems stupid to me, as it would make things alot cleaner in this case esp.)... Thanks again for your time. – MoonKnight Aug 02 '12 at 08:43
  • Ah ok, just on your other question you are creating an empty instance of `KeyValuePair` named `taskPair` which doesn't appear to get used anywhere so I was just making sure you knew what my code was getting at! Yeah seems a bit odd, surely anything that makes your development life easier or speeds it up should *always* be allowed?? Are you still hitting the freezing issue? I wrote the above code without testing or anything therefore it's possible there is a bug in there. Looking at it as it is though there doesn't seem to be anything obvious. – James Aug 02 '12 at 09:01
2

You can implement a "queue" of tasks using a continuation chain. This is easy to read and understand and will work as expected. Also, the "queueing" logic is now contained within your TaskSpin.

private Task lastTask;

public void TaskSpin(Func asyncMethod, object[] methodParameters)
{
    ...
    if(lastTask == null)
        asyncTask = Task.Factory.StartNew<bool>(() => 
            asyncMethod(uiScheduler, methodParameters));
    else 
        asyncTask = lastTask.ContinueWith(t => 
            asyncMethod(uiScheduler, methodParameters));

    lastTask = asyncTask;

    asyncTask.ContinueWith(task =>
    {
        // Finish the processing update UI etc.
    }
    ...
}

This will ensure that each new Task will only run once the last Task has been completed.

Edit: If you want your UI task to be included in the sequential queue, a simple change:

private Task lastTask;

public void TaskSpin(Func asyncMethod, object[] methodParameters)
{
    ...
    if(lastTask == null)
        asyncTask = Task.Factory.StartNew<bool>(() => 
            asyncMethod(uiScheduler, methodParameters));
    else 
        asyncTask = lastTask.ContinueWith(t => 
            asyncMethod(uiScheduler, methodParameters));

    lastTask = asyncTask.ContinueWith(task =>
    {
        // Finish the processing update UI etc.
    }
    ...
}
Dave New
  • 38,496
  • 59
  • 215
  • 394
0

You should probably think about implementing synchronization by locking an object that Run requires before continuing. You would something like the following:

// locking object for write synchronization
object syncObj;

...

public TaskSpin(Func asyncMethod, object[] methodParameters)
{
    ...
    asyncTask = Task.Factory.StartNew<bool>(() => 
        asyncMethod(uiScheduler, methodParameters));

    asyncTask.ContinueWith(task =>
    {
        lock(syncObj)
        {
            // Finish the processing update UI etc.
        }
    }
    ...
}

public void Run()
{
    lock(syncObj)
    {
        // write results to common file
    }
}

Although, I am pretty sure this will NOT guarantee that the tasks complete in order. This sounds like it might be important to you, so possibly my suggestion is not exactly what you want.

  • If each task is processed sequentially then there should be no need for locking. – James Aug 01 '12 at 11:38
  • Yes but a continuation chain approach essentially performs the work in serial and negates the benefit of using the Task Parallel library. – Roger Johnson Aug 01 '12 at 11:49
  • The point I was trying to bring out was that the forced serialization of the work itself as a solution to the Run methods reentrancy problem was not the most efficient way of getting the work done. – Roger Johnson Aug 01 '12 at 11:55
0

If your goal is to serialize updates to the UI, this has to happen anyway because UI can only be updated from the GUI thread. Fortunately, mechanisms to do this are already built in.

I note that you're using winforms. Given that, I suggest using a mechanism such as SafeInvoke. This will queue up code for execution on the GUI thread.

Community
  • 1
  • 1
Greg D
  • 43,259
  • 14
  • 84
  • 117
  • Updating the uI is not the issue here - my TaskSpin method handels this using `TaskScheduler.FromCurrentSynchronizationContext()`. Thanks... – MoonKnight Aug 01 '12 at 12:03