3

I have a WPF app which, upon button click, creates a List<Task<int>> and starts these tasks. My assumption is that the Add() call starts these in parallel, but async.

This is my function that does a bunch of WMI calls in serial on a remote machine:

AgentBootstrapper.cs

public async Task<int> BootstrapAsync(BootstrapContext context, IProgress<BootstrapAsyncProgress> progress)
{
  ...

  do a bunch of stuff in serial *without* await calls

  ...

  if (progress != null)
  {
      progress.Report(new BootstrapAsyncProgress
      {
          MachineName = context.MachineName, 
          ProgressPercentage = 30, 
          Text = "Copying install agent software to \\\\" + context.MachineName + "\\" + context.ShareName
      });
  }

  ...

  return pid; // ProcessId of the remote agent that was just started
}

And this is obviously my button handler in the UI:

Shell.xaml.cs

private async void InstallButton_Click(object sender, RoutedEventArgs e)
{
    var bootstrapTasks = new List<Task<int>>();

    var progress = new Progress<BootstrapAsyncProgress>();
    progress.ProgressChanged += (o, asyncProgress) =>
    {
        Debug.WriteLine("{0}: {1}% {2}", asyncProgress.MachineName, asyncProgress.ProgressPercentage,
            asyncProgress.Text);

        //TODO Update ViewModel property for ProgressPercentage
    };

    var vm = DataContext as ShellViewModel;

    Debug.Assert(vm != null);

    foreach (var targetMachine in vm.TargetMachines)
    {
        var bootstrapContext = new BootstrapContext(targetMachine.MachineName, true)
        {
            AdminUser = vm.AdminUser,
            AdminPassword = vm.AdminPassword
        };

        var bootstrapper = new AgentBootstrapper(bootstrapContext);
        bootstrapTasks.Add(bootstrapper.BootstrapAsync(bootstrapContext, progress)); // UI thread locks up here
    }
}

I know functions marked as async should have function calls within them using await. In my case, these are all calls to some synchronous WMi helper functions which all return void. So, I don't think await is what I want here.

Simply put, I want all the bootstrapTasks items (the calls to bootstrapper.BootstrapAsync() to fire at once, and have the UI thread receive progress events from all of them. When the whole lot are complete, I'll need to handle that too.

Update 1

Attempting to use Task.Run() fixes the UI locking issue, but only the first Task instance is executed. Update foreach loop:

foreach (var targetMachine in vm.TargetMachines)
{
    var tm = targetMachine; // copy closure variable
    var bootstrapContext = new BootstrapContext(tm.MachineName, true)
    {
        AdminUser = vm.AdminUser,
        AdminPassword = vm.AdminPassword
    };

    var bootstrapper = new AgentBootstrapper(bootstrapContext);

    Debug.WriteLine("Starting Bootstrap task on default thread pool...");
    var task = Task.Run(() =>
    {
        var pid = bootstrapper.Bootstrap(bootstrapContext, progress);
        return pid;
    });

    Debug.WriteLine("Adding Task<int> " + task.Id + " to List<Task<int>>.");
    tasks.Add(task);

    await Task.WhenAll(tasks);  // Don't proceed with the rest of this function untill all tasks are complete
}

Update 2

Moving the await Task.WhenAll(tasks); outside the foreach loop allows all tasks to run in parallel.

Mark Richman
  • 28,948
  • 25
  • 99
  • 159
  • I had to do something like this. I used the task completion source and polled the list with linq. Progress reporting was done via a synchronization context that accompanied each task. – Gayot Fow Apr 27 '14 at 23:40
  • @GarryVass I just updated my question with some more info. Is your approach still applicable? – Mark Richman Apr 27 '14 at 23:46
  • @MarkRichman, FYI: http://stackoverflow.com/a/21357567/1768303 – noseratio Apr 27 '14 at 23:53
  • @Noseratio Ok, but if you `await Task.Run()` how do you get a handle to that awaited task? I need to add the task to my `List>`. – Mark Richman Apr 28 '14 at 00:00
  • Stupid me - I just had to move `await Task.WhenAll(tasks);` outside my loop. Works!!!! – Mark Richman Apr 28 '14 at 00:04
  • @MarkRichman, yes, pretty much. I like to add a continue with to each so everything is cleaned up and disposed – Gayot Fow Apr 28 '14 at 00:12
  • Now I just have to get the UI to respect what I'm doing with the ViewModel here. The `ProgressChanged` event fires, and I see the event args in my debug output, but settings the ViewModel properties to those values has no effect on the UI. I suppose that's a new topic though... – Mark Richman Apr 28 '14 at 00:25

2 Answers2

5

Nothing in the code generated for async/await involves the creation of threads. Using the async keyword does not cause another thread to be used. All async does is allow you to use the await keyword. If you want something to happen on another thread, try using Task.Run.

Mike Zboray
  • 39,828
  • 3
  • 90
  • 122
  • I don't particularly care if it's on a different thread or not. I just can't have the main thread lock up. – Mark Richman Apr 27 '14 at 21:53
  • 1
    @MarkRichman My point is if you make a blocking call it is blocking on the UI thread because you never moved anything off that thread. You need to put all that serial blocking code into an action and invoke it with `Task.Run`. – Mike Zboray Apr 27 '14 at 21:55
  • Thanks I think that did it. I replaced the `Add()` call with this line `await Task.Run(() => bootstrapper.BootstrapAsync(bootstrapContext, progress));`. The UI isn't locking up. So, does `BootstrapAsync` even need to be an async method at this point? Also, how will those progress events find their way back up to their event handler which lives on the UI thread? – Mark Richman Apr 27 '14 at 22:03
  • @MarkRichman It would not need to be `async` if you never `await`. The progress events will be raised on the UI thread because the `Progress` class captures the `SynchronizationContext` of the thread on which it is created. It is similar to the way `BackgroundWorker` works with `ReportProgress`. – Mike Zboray Apr 27 '14 at 22:09
  • It looks like it's still blocking after the first iteration in the foreach loop at `int pid = await Task.Run(() => bootstrapper.Bootstrap(bootstrapContext, progress));` I get the progress message back up to the handler, but the other Tasks don't run in parallel. – Mark Richman Apr 27 '14 at 22:57
  • Just updated my question with some more info. See "Update 1" above. – Mark Richman Apr 27 '14 at 23:46
  • Just updated my question with some more info. See "Update 1" above. – Mark Richman Apr 27 '14 at 23:55
1

Run the tasks on the thread pool (using the default task scheduler, that is) and await Task.WhenAll(bootstrapTasks) on them in your UI thread?

zmbq
  • 38,013
  • 14
  • 101
  • 171