【问题标题】:Running a background task and waiting for its result without blocking the UI thread在不阻塞 UI 线程的情况下运行后台任务并等待其结果
【发布时间】:2022-01-20 02:07:18
【问题描述】:

我想在我的 WinForms 应用程序上创建一个启动画面。我创建启动画面表单并运行初始化操作。

public async Task RunApplication()
{
    splash = new SplashWindow();
    splash.Show();
    
    await Task.Run(InitializeAsync);
    
    Application.Run(new frmMain());
}

//the InitializeAsync function
private async Task InitializeAsync()
{
   splash.Status = "Test";
}

//the status property
public string Status
{
    get { return status; }
    set {
        if (status != value)
        {
            status = value;
            lblStatus.InvokeIfRequired(() => lblStatus.Text = value);
        }
    }
}

public static void InvokeIfRequired(this ISynchronizeInvoke snc,
                                         MethodInvoker action)
{
    if (snc.InvokeRequired) {
        snc.Invoke(action, null);
    } else {
        action();
    }
}

如您所见,我希望能够将标签更改为当前状态。但是,当我运行这段代码时,只要我点击snc.Invoke(action, null); 行,程序就会挂起。我做了一些研究/调试,似乎当我执行Task.Run 时,它会阻塞 UI 线程,并且由于在该线程上创建了启动画面,因此程序永远无法在 UI 线程上运行该操作。

我最终做的是等到任务完成并不断执行应用程序事件。

var task = Task.Run(InitializeAsync);

while(!task.IsCompleted)
{
    Application.DoEvents();
    Thread.Sleep(10);
}

Applicaiton.Run(new frmMain());

这行得通。但是,我想知道是否有更优雅的解决方案。

更新:

根据 Stephen Cleary 的回答,我已将代码更改为此,但它不起作用。我怎样才能让它发挥作用?

//on Program.cs file:
[STAThread]
static async Task Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    await Bootstrapper.RunApplicationAsync();
}

//on Bootstrapper.cs class
private static SplashWindow splash;
public static async Task RunApplicationAsync()
{
  splash = new SplashWindow();
  splash.Show();

  await InitializeAsync();

  Applicaiton.Run(new frmMain());
}

private static async Task InitializeAsync()
{
  splash.Status = "Start";
  await Task.Delay(1000); //program stops here
  splash.Status = "Stop";
}

//on the SplashWindow.cs file
public string Status
{
  get => lblStatus.Text;
  set => lblStatus.Text = value;
}

【问题讨论】:

  • 永远不要打电话给Application.DoEvents()。它位于向后兼容 VB6 代码的框架中。它可以轻松创建几乎无法调试的重入代码问题。
  • @Enigmativity 我知道这就是我问这个问题的原因。感觉这不是一个优雅的解决方案,所以我正在寻找一种方法来摆脱它。

标签: c# multithreading winforms async-await


【解决方案1】:

首先,摆脱InvokeIfRequired。 InvokeRequired - 不管它有多常见 - 都是严重的代码异味。

相反,你可以去掉Task.Run,直接调用InitializeAsync:

public async Task RunApplication()
{
  splash = new SplashWindow();
  splash.Show();

  await InitializeAsync();

  Applicaiton.Run(new frmMain());
}

private async Task InitializeAsync()
{
  splash.Status = "Test";
}

public string Status
{
  get => lblStatus.Text;
  set => lblStatus.Text = value;
}

如果您出于某种原因确实需要使用Task.Run(即,如果InitializeAsync 有CPU 绑定或阻塞工作要做),那么您可以使用Progress<T>:

public async Task RunApplication()
{
  splash = new SplashWindow();
  splash.Show();

  var progress = new Progress<string>(update => splash.Status = update);
  await Task.Run(() => InitializeAsync(progress));

  Applicaiton.Run(new frmMain());
}

private async Task InitializeAsync(IProgress<string>? progress)
{
  progress?.Report("Test");
}

public string Status
{
  get => lblStatus.Text;
  set => lblStatus.Text = value;
}

【讨论】:

  • 我需要在另一个线程中调用该函数,因为它使用的库不支持异步并且会阻塞 UI 线程。你说我应该摆脱InvokeIfRequired。如果我试图从另一个线程访问该属性怎么办?也就是说,第一部分按预期工作。但是,当我添加Task.Run 部分时,即使我删除了InitializeAsync 函数的全部内容,应用程序也无法越过该行。如果我只是像await InitializeAsync(); 一样调用它,但在函数内添加Task.Run,同样的事情。
  • 顺便说一句,我正在运行 Program.cs 文件中的 RunApplication 函数,该文件位于 [STAThread] static async Task Main() 主文件中。
  • 请看更新
  • 您的问题是您试图在不进入 UI 主循环 (Application.Run) 的情况下显示 UI。您需要构建您的启动/主表单,以便正常输入主循环。
  • 我该怎么做?可以举个例子吗?
猜你喜欢
  • 1970-01-01
  • 2015-09-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-05
  • 2015-12-07
  • 2013-09-12
  • 1970-01-01
相关资源
最近更新 更多