后台线程消息到 UI 线程缓存,直到后台进程完成才推送到 UI

Background Thread messages to UI Thread cache and not pushed to UI till background process complete

我已经阅读了许多关于 .NET 中多线程的先前帖子,并尝试了多种方法,但我正在努力让我的特定案例和 c#.NET 代码正常运行。我的代码设计用于在后台 运行 长 运行ning 进程,并将正在进行的进度报告到我的 Windows 表单 UI 中的日志显示和进度条控件相同的形式。截至目前它正在运行,但 UI 线程正在锁定并且消息不会被推送到 UI 直到后台进程完成。

为了简洁起见,我将在这里总结当前的框架...

用户界面表单

在初始化期间,我们将同步上下文设置如下...

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

该表单还包括一个自定义事件处理程序,它将 Post 回调包装到同步上下文 ...

// 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);
}

后台线程

为了启动后台进程,我们使用了利用 .ConfigureAwait(false) 的异步等待调用,它深入到控制器“执行”任务方法。此方法包括多个“InvokeMessage”调用,这些调用调用上面的 UI 处理程序附加到的事件 ...

// 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);
}

我对此束手无策。多年来,异步等待和多线程实现的各种实践发生了翻天覆地的变化,并且令人困惑。解开不同的实现以查看适用于我的案例的是什么,这一直是我苦苦挣扎并觉得自己做空的地方。我非常感谢任何建设性的帮助,它们可以为我指明正确的方向,以防止我的 UI 锁定并防止我的消息缓存而没有立即被推送到 UI .

首先,.ConfigureAwait(false) 所做的是避免在与配置调用之前相同的同步上下文上继续编组配置调用。所以你所做的描述和你发布的代码显然不匹配。

这里有一个简单的例子,可以帮助您理解它是如何工作的。

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

现在 .ConfigureAwait(false)

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

假设方法 DoSomething() 执行异步操作,这也是正确的。

.ConfigureAwait(false) 对您的 Execute() 方法内部发生的事情没有影响,所以我认为您可以删除它。

现在,await在一个单独的线程上 运行 不会运行 处理某些内容。您可能想要的更像是:

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);

注意方法Execute()不需要return一个Task,方法本身被完成将指示TaskTask.Factory.StartNew()创建已完成。

另请注意,您无需手动设置同步上下文。在你的情况下,它只是一个空操作,因为 Execute 与它的调用者在同一个线程上 运行ning,在我描述的情况下 不能 是完成是因为 Execute 运行 在不同的线程上。

如果你不能改变Execute方法的原型,那么你可以这样做:

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);
}

此外,为了帮助您理解 SynchornizationContext,您可以将其视为一个线程执行另一个线程请求的某些内容的一种方式。因此,如果线程 A 希望线程 B 运行 某些东西(在线程 B 上),则线程 A 必须获得线程 B 的 SynchronizationContext 实例,因此线程 A 可以将某些东西推送到线程 B 的执行队列中.请注意,如果 B 的 SynchronizationContext 没有将执行委托给另一个线程(例如 ThreadPool),而是 运行 推送的(Posted)作业本身(在其自己的线程上),则这适用。

最后一件事,如果你 运行 一个方法 returning 一个 Task 但那个方法只执行同步操作,整个方法将只是 运行 同步,因此,在同一个线程上。