简体   繁体   中英

Running a background task and waiting for its result without blocking the UI thread

I want to create a Splash Screen on my WinForms application. I create the splash screen form and run the initialization operation.

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

As you can see, I want to be able to change the label to the current status. However, when I run this code, as soon as I hit the snc.Invoke(action, null); line, the program hangs. I did some research/debugging and it seems that when I do Task.Run , it will block the UI thread and since the splash has been created on that thread, the program is never able to run that action on the UI thread.

What I ended up doing was to wait until the task is finished and constantly do the application events.

var task = Task.Run(InitializeAsync);

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

Applicaiton.Run(new frmMain());

This works. However, I was wondering if there's a more elegant solution for this.

First, get rid of InvokeIfRequired . InvokeRequired - regardless of how common it is - is a serious code smell.

Instead, you can get rid of Task.Run and just call InitializeAsync directly:

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

If you do need to use Task.Run for some reason (ie, if InitializeAsync has CPU-bound or blocking work to do), then you can use 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;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM