Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problems with scheduling tasks from a UI .continuewith task

Tags:

c#

task

My application schedules a long running task using the following code:

Task.Factory.StartNew<bool>((a) => WorkTask1(),
        TaskCreationOptions.LongRunning | TaskCreationOptions.AttachedToParent)
    .ContinueWith(antecedent => WorkCompletedTask1(antecedent.Result),
        TaskScheduler.FromCurrentSynchronizationContext());

WorkCompletedTask1 is scheduled and displays results on the UI as expected. Depending on results from WorkTask1, WorkCompletedTask1 may schedule additional tasks using the following statement:

Task.Factory.StartNew<bool>((a) => WorkTask2(),
        TaskCreationOptions.LongRunning | TaskCreationOptions.AttachedToParent)
    .ContinueWith(antecedent => WorkCompletedTask2(antecedent.Result),
        TaskScheduler.FromCurrentSynchronizationContext());

WorkTask2 does NOT run on a separate thread as expected; it runs on the UI thread which is blocked until WorkTask2 completes. I thought the TaskCreationOptions.LongRunning would guarantee a separate thread.

Any suggestions on why this does not work? I can schedule addition tasks from UI and non-UI tasks, just not from a .continuewith task in the UI.

Broken Sample Project Code

In an empty Windows Forms project with button1 button on the form, this code does NOT work as expected (Windows 7 VS2010 Express Net 4.0). T2 and T3 run in the UI thread, not a worker thread. Add a listBox1 to your button1 form and try the following:

private delegate void DelegateSendMsg(String msg);
private DelegateSendMsg m_DelegateSendMsg;
private TaskScheduler uiSched;
private Process thisProcess;
private string
    thisProcessName,
    thisProcessId,
    uiThreadName,
    nonuiStatus = "Non-UI",
    uiStatus = "UI";

private void Form1_Load(object sender, EventArgs e)
{
    thisProcess = Process.GetCurrentProcess();
    thisProcessName = thisProcess.ProcessName;
    thisProcessId = thisProcess.Id.ToString();
    uiThreadName = CurrentThread;
    m_DelegateSendMsg = this.SendMsg;
    uiSched = TaskScheduler.FromCurrentSynchronizationContext();
    SendMsg("UI thread name is " + CurrentThread);
}

//create the name of the current task
public string CurrentThread
{
    get
    {
        string threadId = null;
        if (String.IsNullOrEmpty(Thread.CurrentThread.Name))
            threadId = thisProcess.Id.ToString() + "=" + thisProcessName;
        else
            threadId = thisProcessId
                + "=" + thisProcessName
                + "/" + Thread.CurrentThread.Name;
        threadId += ":" + Thread.CurrentThread.ManagedThreadId + " ";
        return threadId;
    }
}

//validate if the function is running in the expected UI state or not
public bool MeetsUIExpectations(string functionName, string expectedStatus)
{
    bool rc = true;
    string currentThreadName = CurrentThread;
    string text = 
        "Function " + functionName + " running in thread " + currentThreadName;
    if ((currentThreadName == uiThreadName) & expectedStatus == uiStatus)
        text += ": UI status as expected";
    else if ((currentThreadName != uiThreadName) & expectedStatus == nonuiStatus)
        text += ": non-UI status as expected";
    else
    {
        text += ": UI status is NOT as expected!"
            + "  Expected: " + expectedStatus
            + "; running in thread" + currentThreadName;
        rc = false;
    }
    SendMsg(text);
    return rc;
}

//display a single text message
private void SendMsg(String msg)
{   
    if (this.InvokeRequired)
        try { this.Invoke(m_DelegateSendMsg, "UI context switch: " + msg); }
        catch (Exception) { }
    else
    {
        listBox1.Items.Add(msg);
        listBox1.TopIndex = listBox1.Items.Count - 1;
    }
}

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew<bool>((a) =>
        T1(), TaskScheduler.Default,
            TaskCreationOptions.LongRunning | TaskCreationOptions.AttachedToParent)
        .ContinueWith(antecedent => T1Completed(antecedent.Result), uiSched);
}

private bool T1()
{
    //get the name of the currently running function and validate UI status
    var currentMethod = System.Reflection.MethodInfo.GetCurrentMethod();
    MeetsUIExpectations(currentMethod.ToString(), nonuiStatus);

    int i = 0;
    while (i < Int32.MaxValue) i++;
    return true;
}

private void T1Completed(bool successful)
{
    var currentMethod = System.Reflection.MethodInfo.GetCurrentMethod();
    MeetsUIExpectations(currentMethod.ToString(), uiStatus);
    if (successful)
    {
        Task.Factory.StartNew<bool>((a) =>
            T2(), TaskScheduler.Default,
                TaskCreationOptions.LongRunning | TaskCreationOptions.AttachedToParent)
            .ContinueWith(antecedent => T2Completed(antecedent.Result), uiSched);
    }
}

private bool T2()
{
    var currentMethod = System.Reflection.MethodInfo.GetCurrentMethod();
    MeetsUIExpectations(currentMethod.ToString(), nonuiStatus);
    int i = 0;
    while (i < Int32.MaxValue) i++;
    return true;
}

private void T2Completed(bool successful)
{
    var currentMethod = System.Reflection.MethodInfo.GetCurrentMethod();
    MeetsUIExpectations(currentMethod.ToString(), uiStatus);
    Task.Factory.StartNew<bool>((a) =>
        T3(), TaskScheduler.Default,
            TaskCreationOptions.LongRunning | TaskCreationOptions.AttachedToParent)
        .ContinueWith(antecedent => T3Completed(antecedent.Result), uiSched);
}

private bool T3()
{
    var currentMethod = System.Reflection.MethodInfo.GetCurrentMethod();
    MeetsUIExpectations(currentMethod.ToString(), nonuiStatus);
    int i = 0;
    while (i < Int32.MaxValue) i++;
    return true;
}

private void T3Completed(bool successful)
{
    //get the name of the currently running function and validate UI status
    var currentMethod = System.Reflection.MethodInfo.GetCurrentMethod();
    MeetsUIExpectations(currentMethod.ToString(), uiStatus);
    SendMsg("All functions completed");
}
like image 759
user1912926 Avatar asked Feb 19 '23 03:02

user1912926


1 Answers

In .NET 4.0 you have to pass TaskScheduler.Default explicitly. You picked the wrong overload to to this (see below).

Some General Stuff

In a continuation on the UI thread the TaskScheduler is still UI thread returned by FromCurrentSynchronizationContext method. Therefore, all new Tasks you start are scheduled on the UI thread too unless you pass a TaskScheduler explicitly:

Here is a code sample:

Task.Factory.StartNew(foo => {}, TaskScheduler.Default)

Feel free to use whatever TaskScheduler you need, but you need to state it explicitly.

Get the Right Overload

There are quite a few overloads for StartNew<T>. In your code below you pick the wrong one, which causes TaskScheduler.Default to act as state (a value passed as a to T3) rather than the actual scheduler:

var options = TaskCreationOptions.LongRunning
    | TaskCreationOptions.AttachedToParent;

// overload with Func<bool>, CancellationToken, options and TaskScheduler
Task.Factory.StartNew<bool>(() => T2(), new CancellationToken(),
    options, TaskScheduler.Default);

// overload with Func<object, bool> with state and options
// scheduler acts as state here instead of actual scheduler, and
// is therefore just passed as (a) to T3 (state is an object, thus
// can also be a TaskScheduler instance)
Task.Factory.StartNew<bool>((a) => T3(),
    TaskScheduler.Default, // state, not scheduler
    options);

Obviously this way you won't get the scheduling you want, but the default behaviour described above.

Additional Information for .NET 4.5

In .NET 4.5, there is TaskContinuationOptions.HideScheduler to change this behavior. See New TaskCreationOptions and TaskContinuationOptions in .NET 4.5 by Stephen Toub for more details on the new options and let me quote a code sample out of it:

// code sample copied from blog post stated above
Task.Factory.StartNew(() => 
{ 
    // #2 long-running work, so offloaded to non-UI thread 
}).ContinueWith(t => 
{ 
    // #3 back on the UI thread 
    Task.Factory.StartNew(() => 
    { 
        // #4 compute-intensive work we want offloaded to non-UI thread (bug!) 
    }); 
}, CancellationToken.None,
TaskContinuationOptions.HideScheduler, // <-- new option stated in text
TaskScheduler.FromCurrentSynchronizationContext()); 

Working Sample Project Code

In an empty Windows Forms project with button1 button on the form, this code works as expected (Windows 7, .NET 4.0):

private void button1_Click(object sender, EventArgs e)
{
    var uiSched = TaskScheduler.FromCurrentSynchronizationContext();

    button1.Enabled = false;

    // this HardWork-task is not blocking, as we have
    // TaskScheduler.Default as the default scheduler
    Task.Factory.StartNew(HardWork)
        .ContinueWith(t =>
        {
            button1.Enabled = true;

            // this HardWork-task will block, as we are on the
            // UI thread scheduler
            Task.Factory.StartNew(HardWork)
                .ContinueWith(t2 =>
                {
                    button1.Enabled = false;

                    // this one will not, as we pass TaskScheduler.Default
                    // explicitly
                    Task.Factory.StartNew(HardWork,
                        new CancellationToken(),
                        TaskCreationOptions.None,
                        TaskScheduler.Default).ContinueWith(t3 =>
                        {
                            button1.Enabled = true;
                        }, uiSched);  // come back to UI thread to alter button1
                }, uiSched); // come back to UI thread to alter button1
        }, uiSched); // come back on UI thread to alter button1
}

public void HardWork()
{
    int i = 0;
    while(i < Int32.MaxValue) i++;
}
like image 121
Matthias Meid Avatar answered Feb 21 '23 02:02

Matthias Meid