【问题标题】:Background Thread messages to UI Thread cache and not pushed to UI till background process complete后台线程消息到 UI 线程缓存并且在后台进程完成之前不会推送到 UI
【发布时间】:2021-10-22 08:24:45
【问题描述】:

我已经阅读了许多以前关于 .NET 中的多线程的帖子,并尝试了多种方法,但我很难让我的特定案例和 c#.NET 代码正常运行。我的代码旨在在后台运行长时间运行的进程,并将正在进行的进度报告到我的 Windows 窗体 UI 中的日志显示和同一窗体上的进度条控件中。目前它可以运行,但是 UI 线程被锁定,并且在后台进程完成之前不会将消息推送到 UI。

为了简洁起见,我在这里总结一下目前的框架……

用户界面表单

在初始化过程中,我们将 Synchronization 上下文设置如下 ...

// 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) 的 async-await 调用,该调用深入到控制器的“执行”任务方法。此方法包括几个“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 的情况下缓存。

【问题讨论】:

  • 只使用以下之一:(异步和任务)或(BackgroundWorker 和事件)或(线程和 EventWaitHandles)。不要尝试混合使用异步和事件等技术,否则您的程序会变得令人难以置信的混乱。
  • @DourHighArch 谢谢你的警告。截至目前,我相信代码中有足够的分离,这些方法不会混合产生不利影响。但是注意事项很好。

标签: c# .net multithreading async-await synchronizationcontext


【解决方案1】:

首先,.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() 方法内部发生的事情没有影响,所以我认为您可以删除它。

现在,awaiting 某些东西不会让它在单独的线程上运行。您可能想要的更像是:

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()方法不需要返回Task,方法本身正在完成会表明Task.Factory.StartNew()创建的Task已经完成。

另外请注意,您不需要手动设置同步上下文。在您的情况下,这只是一个无操作,因为 Execute 与其调用者在同一个线程上运行,在我描述的情况下,不能因为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) 作业,则这适用。

最后一件事,如果您运行一个返回 Task 的方法,但该方法只执行同步操作,则整个方法将同步运行,因此在同一个线程上运行。

【讨论】:

  • 感谢您的清晰解释。这与我们拥有的其他代码相结合,这些代码使用 System.Diagnostics.Process 启动异步 shell Python 脚本,并帮助填补我怀疑可能存在的缺失部分。明天我将尝试 Task.Factory.StartNew() 模式并报告它的进展情况。
  • 好的,我可以很高兴地报告 Task.Factory.StartNew() 模式通过将“执行”进程发送到后台线程并释放 UI 来解决问题。非常感谢!
  • 你应该使用Task.Run而不是更低级别的Task.Factory.StartNew
  • @StephenCleary 我同意你的观点,但在他/她的情况下,我认为使用LongRunning 运行以在专用线程上运行是首选,因此使用Task.Factory.StartNew。对于可能感兴趣的人:kevingosse.medium.com/…Task.Run / Task.Factory.StartNew 部分很好读)
  • @SebastienROBERT:我还是不同意。线程池将在一两秒后重新平衡,此时它与使用专用线程相同。同时,StartNew 的用法很危险,因为它没有明确指定任务调度程序;每个人都应该在调用StartNew 时始终指定TaskScheduler,因为默认的任务调度程序可能会导致令人惊讶的行为。 IMO 更安全的默认值 Task.Run 远远超过了 StartNew 的微小性能优势。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-05-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多