Home > Back-end >  Background Thread messages to UI Thread cache and not pushed to UI till background process complete
Background Thread messages to UI Thread cache and not pushed to UI till background process complete

Time:10-23

I've read many previous posts on multithreading in .NET, and tried a number of approaches but I'm struggling to get my specific case and c#.NET code to behave. My code is designed to run long-running processes in the background and report ongoing progress into a log display in my Windows Forms UI and to a progress bar control on the same form. As of right now it's operational, but the UI thread is locking up and the messages don't get pushed to the UI till the background process reaches completion.

For the sake of brevity, I'll summarize the current framework here...

User Interface Form

During initialization, we set the Synchronization context as follows ...

// reference to synchronization context within the UI initialization     
Form_StepProcessor.SynchronizationContext = 
    (WindowsFormsSynchronizationContext)System.Threading.SynchronizationContext.Current;

The form also includes a custom event handler which wraps a Post callback to the syncrhonization context ...

// attach asynchronous result processing handler within the UI...
try
{
    AbsNhdPlusDotNetControllerAsync.ProcessResult -= this.OnProcessDotNetAsyncResult;
}
catch
{ // ignore
}
finally
{
    AbsNhdPlusDotNetControllerAsync.ProcessResult  = this.OnProcessDotNetAsyncResult;
}
// process result event declared within the controller
public delegate void ProcessResultEvent(object sender, ProcessResultEventArgs e);
public static event ProcessResultEvent ProcessResult;
public class ProcessResultEventArgs : EventArgs
{
    public Exception Exception { get; set; }

    public enpls_ProcessStatus Status { get; set; } = enpls_ProcessStatus.Failure;

    public string Message { get; set; } = string.Empty;

    public int PercentComplete { get; set; } = 0;
}
// event handler within the UI to push messaging to log control and increment progress bar
private void OnProcessDotNetAsyncResult(object sender, AbsNhdPlusDotNetControllerAsync.ProcessResultEventArgs e)
{
    Form_StepProcessor.SynchronizationContext.Post(new SendOrPostCallback(x =>
    {
        // DO STUFF (i.e. post messages to display, increment progress bar, etc.)...
    }), e);
}

Background Thread

To launch the background process, we're using async-await calls leveraging .ConfigureAwait(false) which drill down to a controller "Execute" task method. This method includes several "InvokeMessage" calls which invoke the event to which the UI handler above is attached ...

// call internal to the controller which immediately calls the "Execute" task method 
await this.Execute(pModel_Validation, sTask).ConfigureAwait(false);
protected override Task Execute(iNhdPlusBaseModel pModel, string sTask)
{
    // set the synchronization context (this didn't seem to do anything)
    SyncrhonizationContext.SetSynchronizationContext(Form_StepProcessor.SynchronizationContext);

    // do stuff ...

    // invoke log message ...
    this.InvokeMessage("here's a sample log message!", 10);

    // do stuff ...

    // invoke log message ...
    this.InvokeMessage("here's a second sample log message!", 20);

    // etc.

    return Task.CompletedTask;
}
public void InvokeMessage(string sMessage, int iPercentComplete)
{
    var pDateTime = DateTime.Now.ToLocalTime();
    var sHour = pDateTime.Hour.ToString("D2");
    var sMinute = pDateTime.Minute.ToString("D2");
    var sSecond = pDateTime.Second.ToString("D2");
    var pArgs = new ProcessResultEventArgs
    {
        Status = enpls_ProcessStatus.Message,
        Message = $"{sHour}:{sMinute}:{sSecond}->{sMessage}",
        PercentComplete = iPercentComplete
    };
    AbsNhdPlusDotNetControllerAsync.ProcessResult?.Invoke(this, pArgs);
}

I'm at my wits end with this. The various practices underlying async-await and multi-threading implementations have drastically changed over the years, and are confusing. Unraveling different implementations to see what applies to my case has been where I'm struggling and feel that I'm coming up short. I'd very much appreciate any constructive help that can point me in the right direction for an approach that will prevent my UI from locking up and prevent my messaging from caching up without promptly being pushed to the UI.

CodePudding user response:

First, what .ConfigureAwait(false) does is avoiding marshalling continuation of the configured call on the same synchronization context as before the configured call. So the description you make and the code you posted are clearly mismatching.

Here is a simple example to help you understand how it works.

Console.WriteLine($"Thread: {Thread.CurrentThead.ManagedThreadId}"); // Thread: X
await DoSomething();
Console.WriteLine($"Thread: {Thread.CurrentThead.ManagedThreadId}"); // Thread: X

Now with .ConfigureAwait(false)

Console.WriteLine($"Thread: {Thread.CurrentThead.ManagedThreadId}"); // Thread: X
await DoSomething().ConfigureAwait(false);
Console.WriteLine($"Thread: {Thread.CurrentThead.ManagedThreadId}"); // Thread: Y

This holds true assuming the method DoSomething() does something asynchronous.

The .ConfigureAwait(false) has no impact on what's going on inside your Execute() method, so I think you can just remove it.

Now, awaiting something does not make it run on a separate thread. What you may want for that is more something like:

protected override void Execute(iNhdPlusBaseModel pModel, string sTask)
{
    // do stuff ...

    // invoke log message ...
    this.InvokeMessage("here's a sample log message!", 10);

    // do stuff ...

    // invoke log message ...
    this.InvokeMessage("here's a second sample log message!", 20);

    // etc.
}

...

await Task.Factory.StartNew(() => Execute(pModel_Validation, sTask), TaskCreationOptions.LongRunning);

Note that the method Execute() does not need to return a Task, the method itself being finished will indicate the Task created by Task.Factory.StartNew() is completed.

Also note that you do not need to set the synchronization context manually. In your case it was just a no-op because Execute was running on the same thread as its caller, in the case I describe it must not be done because Execute runs on a different thread.

If you cannot change the prototype of the Execute method, then you can do the following:

protected override Task Execute(iNhdPlusBaseModel pModel, string sTask)
{
    return Task.Factory.StartNew(() => {
        // do stuff ...

        // invoke log message ...
        this.InvokeMessage("here's a sample log message!", 10);

        // do stuff ...

        // invoke log message ...
        this.InvokeMessage("here's a second sample log message!", 20);

        // etc.
    }, TaskCreationOptions.LongRunning);
}

Also, to help you understand the SynchornizationContext, you can see it as a way for a thread to execute something requested by another thread. So if thread A wants thread B to run something (on thread B), the thread A have to get a SynchronizationContext instance of the thread B, and so thread A can push something in thread B's execution queue. Note that this holds true if B's SynchronizationContext does not delegate execution to another thread (e.g. ThreadPool) but run the pushed (Posted) job on itself (on its own thread).

One last thing, if you run a method returning a Task but that method only perform synchronous actions, the whole method will just run synchronously, and therefore, on the same thread.

  • Related