异步等待似乎使用UI线程。

4
在视图模型中,我使用了一个工厂:
private async Task<BaseData> InitializeAsync()
{
    await InstancesAsync();
    await ProjectsAsync();
    await AdminAsync();
    return this;
}
public static async Task<BaseData> CreateAsync()
{
    var ret = new BaseData();
    return await ret.InitializeAsync();
}

期待的方法非常直截了当,例如:
 var instances = await TaskEx.Run(new Func<List<string>>(() => Agent.GetInstances()));

在WPF视图中,我希望在构造函数中设置DataContext:
Loaded += delegate
{
    Dispatcher.Invoke(new Action(async () => { DataContext = await BasisGegevens.CreateAsync(); }));
};

尽管它能够工作,但我感到非常不舒服,因为UI线程被广泛使用,包括在回调完成后等待的情况。我错过了什么吗?
此外,我不明白如何使用工厂模式来创建DataContext,因为如果没有上面的Invoke,我会收到一个不同线程拥有该对象的错误。
编辑:使用Cleary先生的想法,我得到了:
Loaded += async (object sender, RoutedEventArgs e) =>
          { DataContext = await BaseData.CreateAsync(); };
public static Task<BaseData> CreateAsync()
{
    var ret = new BaseData();
    return ret.InitializeAsync();
}
private async Task<BaseData> InitializeAsync()
{
    // UI thread here
    await InstancesAsync().ConfigureAwait(false);
    // thread 'a' here
    await ProjectsAsync().ConfigureAwait(false);
    // thread 'a' sometimes 'b' here
    await AdminAsync().ConfigureAwait(false);
    // thread 'a' or 'b' here
    return this;
}

这段代码运行良好,但我无法理解 ConfigureAwait(false) 的工作原理。
在方法 InstancesAsync() 中,我有一个等待的任务:
var instances = await TaskEx.Run(new Func<List<string>>(() => Agent.GetInstances()));
在等待响应后,我返回到UI线程 - 我从未预料到会发生这种情况!
请注意,ProjectsAsync()AdminAsync() 的行为相同,尽管它们在工作线程(或后台线程)上启动!
我以为 ConfigureAwait(true) 会在调用线程中返回(在我的情况下是UI线程)。我测试了一下,确实如此。
但是,由于嵌套的等待,请看注释,为什么我也会看到这个问题出现在 ConfigureAwait(false) 中。


你展示的代码中,“Loaded”事件的源是什么?如果这是一个常规的FrameworkElement.Loaded事件,它应该在UI线程上触发。你是否在单独的线程上创建视图?那可能是因为你在非UI池线程上调用了Agent.GetInstances()吗? - noseratio - open to work
Loaded事件是一个常规的FrameworkElement.Loaded事件。我原本打算在UI线程上创建视图,但显然是错的。Agent.GetInstances()在一个异步任务中运行,我认为它应该在非UI线程池上运行,但实际上并不是这样。 - Gerard
TaskEx.Run 使用一个独立的线程池来运行你的 Func 委托,这就是 Agent.GetInstances() 被执行的地方。我建议你遵循 Stephen 的回答,并确保 WPF UI 元素和 ViewModel 对象都在同一个 - UI - 线程上创建。添加一些记录 (System.Threading.Thread.CurrentThread.ManagedThreadId) 来查看你所在的线程和位置。 - noseratio - open to work
1
ConfigureAwait(false) 只影响特定的 await,对于“内部”的 await 没有影响。 - svick
@svick:当然,谢谢,我也已经检查过了。 - Gerard
1个回答

7

我认为将ViewModel视为具有UI线程关联性最为实用。即使它不是真正的UI,也应该将其视为逻辑UI。因此,所有ViewModel类中的属性和可观察集合更新都应在UI线程上完成。

在您的async方法中,如果不需要返回到UI线程,则可以使用ConfigureAwait(false)来避免在UI线程上恢复。例如,如果您的各种初始化方法是独立的,则可以像这样做:

private async Task<BaseData> InitializeAsync()
{
  // Start all methods on the UI thread.
  var instancesTask = InstancesAsync();
  var projectsTask = ProjectsAsync();
  var adminTask = AdminAsync();

  // Await for them all to complete, and resume this method on a background thread.
  await Task.WhenAll(instancesTask, projectsTask, adminTask).ConfigureAwait(false);

  return this;
}

此外,每当你使用return await时,请再次检查以查看是否可以完全避免使用async/await

public static Task<BaseData> CreateAsync()
{
  var ret = new BaseData();
  return ret.InitializeAsync();
}

最后,强烈建议避免使用Dispatcher。你的Loaded事件可以简化为以下内容:
Loaded += async ()
{
  DataContext = await BasisGegevens.CreateAsync();
};

我从您在“http://blog.stephencleary.com/2013/01/async-oop-2-constructors.html”上的工厂模式中推导出了`return await`。 - Gerard
Task.WhenAll:任务必须按照确定的顺序运行。 - Gerard
Loaded委托必须写成async (object sender, RoutedEventArgs e)才能编译。 - Gerard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接