Using a Task Continuation to Stack Jobs

2019-08-02 23:29发布

问题:

All, I have a mathod called TaskSpin that runs the passed method on a background thread using TPL.

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

    asyncTask.ContinueWith(task =>
    {
        ...
        // Finish the processing update UI etc.
        ...
        if (isStacked && task.Status == TaskStatus.RanToCompletion)
            ProcessNextTask(dataGridViewSite);
    }
    ...
}

This routine is well established to fire off one method at-a-time, but I have recently been required to queue multiple methods and run them sequentially. To do this (aided by this answer) I have written the following button click event handler

private void buttonProcAllUrg_Click(object sender, EventArgs e)
{
    // Build the stacker.
    Dictionary<Func<TaskScheduler, object[], bool>, object[]> taskPair = 
        new Dictionary<Func<TaskScheduler, object[], bool>, object[]>();
    this.taskQueue = 
        new Queue<KeyValuePair<Func<TaskScheduler, object[], bool>, object[]>>();

    // Populate the stacker.
    foreach (DataGridViewRow row in this.DataGridViewDrg.Rows) 
    {
        KeyValuePair<Func<TaskScheduler, object[], bool>, object[]> task =
            new KeyValuePair<Func<TaskScheduler, object[], bool>, object[]>
            (BuildDrgDataForSite, DrgDataRowInfo(row.Index));     
        this.taskQueue.Enqueue(task); 
    }

    // Launch the queue.
    ProcessNextTask(this.DataGridViewDrg);
}

and the method ProcessNextTask is defined as

private void ProcessNextTask(DataGridView dataGridView) 
{     
    try     
    {
        // Queue empty.
        bIsStacked = true;
        if (this.taskQueue.Count <= 0)
        {
            bIsStacked = false;
            return;
        }

        // Launch new task.
        KeyValuePair<Func<TaskScheduler, object[], bool>, object[]> item = this.taskQueue.Dequeue();
        Task<bool> asyncBuildDataTask = null;
        TaskSpin(asyncBuildDataTask, uiScheduler, mainForm,
                    item.Key, item.Value, dataGridView,
                    "Process stack successfully executed", true, bIsStacked);
    }     
    catch(InvalidOperationException)    
    {
        // Nothing left in stack.
        bIsStacked = false;    
    }   
} 

This works fine, but after the first task has run and the ProcessNextTask is called for the second (or more times) from the continuation, the GUI becomes unresponsive. What can I do to ensure the UI thread is not blocked upon the second call?

Note. I have tried to launch the ProcessNextTask method on another thread using the UI thread synchronisation context

Task task = Task.Factory.StartNew(() =>
{
    ProcessNextTask(dataGridView);
}, CancellationToken.None, 
   TaskCreationOptions.LongRunning, 
   uiScheduler);

where TaskScheduler uiSchduler = TaskScheduler.FromCurrentSynchonisationContex();.

Edit: I have attampted to create a BlockingCollection to facilitate what I want to do based on @Reed Copsey's answer below, but I have never done this and would welcome any advice here

BlockingCollection<Action<object[]>> taskQueue = 
    new BlockingCollection<Action<object[]>>();
foreach (DataGridViewRow row in this.DataGridViewDrg.Rows) 
{
    Action<object[]> tmpAction = del => 
        {AsyncBuildDrgDataForSite(DrgDataRowInfo(row.Index)); };
    taskQueue.Add(tmpAction);
}

Thanks for your time.

回答1:

I would recommend thinking about this differently.

If you're trying to "stack jobs", it's typically better to rework the routine into a producer/consumer scenario. BlockingCollection<T> provides the proper framework for this, as it provides a thread safe queue which you can just use to add in your work items, and then create a task to process them (via GetConsumingEnumerable()). Once the items are done, you can update your UI, then restart the "producer" task as you add a new item, etc.