等待任务完成而不阻塞 UI 线程

Wait for Task to Complete without Blocking UI Thread

我有一个相当复杂的 WPF 应用程序(很像 VS2013)有 IDocumentsITools 停靠在应用程序的主要 shell 中。其中一个 Tools 需要在主 Window 关闭时安全关闭以避免进入 "bad" 状态。所以我使用 Caliburn Micro 的 public override void CanClose(Action<bool> callback) 方法来执行一些数据库更新等。我遇到的问题是此方法中的所有更新代码都使用 MongoDB Driver 2.0 而这个东西是 async。一些代码;目前我正在尝试执行

public override void CanClose(Action<bool> callback)
{
    if (BackTestCollection.Any(bt => bt.TestStatus == TestStatus.Running))
    {
        using (ManualResetEventSlim tareDownCompleted = new ManualResetEventSlim(false))
        {
            // Update running test.
            Task.Run(async () =>
                {
                    StatusMessage = "Stopping running backtest...";
                    await SaveBackTestEventsAsync(SelectedBackTest);
                    Log.Trace(String.Format(
                        "Shutdown requested: saved backtest \"{0}\" with events",
                        SelectedBackTest.Name));

                    this.source = new CancellationTokenSource();
                    this.token = this.source.Token;
                    var filter = Builders<BsonDocument>.Filter.Eq(
                        BackTestFields.ID, DocIdSerializer.Write(SelectedBackTest.Id));
                    var update = Builders<BsonDocument>.Update.Set(BackTestFields.STATUS, TestStatus.Cancelled);
                    IMongoDatabase database = client.GetDatabase(Constants.DatabaseMappings[Database.Backtests]);
                    await MongoDataService.UpdateAsync<BsonDocument>(
                        database, Constants.Backtests, filter, update, token);
                    Log.Trace(String.Format(
                        "Shutdown requested: updated backtest \"{0}\" status to \"Cancelled\"",
                        SelectedBackTest.Name));
                }).ContinueWith(ant =>
                    {
                        StatusMessage = "Disposing backtest engine...";
                        if (engine != null)
                            engine.Dispose();
                        Log.Trace("Shutdown requested: disposed backtest engine successfully");
                        callback(true);
                        tareDownCompleted.Set();
                    });
            tareDownCompleted.Wait();
        }
    }
}

现在,首先我没有 ManualResetEventSlim,这显然会 return 到 CanClose 调用者,然后我在后台更新我的数据库 [thread-pool]线。为了在我完成更新之前阻止 return,我尝试阻止 return,但这会冻结 UI 线程并阻止任何事情发生。

如何在不过早 return 调用方的情况下将清理代码发送到 运行?

感谢您的宝贵时间。


请注意,我无法使用异步签名覆盖 OnClose 方法,因为调用代码不会等待它(我无法控制它)。

除了阻止 return,我认为你别无选择。然而,尽管 UI 线程被锁定,您的更新仍应 运行。我不会使用 ManualResetEventSlim,而只是一个简单的 wait() 和一个没有继续的任务。原因是默认情况下 Task.Run 阻止子任务(您的延续)附加到父任务,因此您的延续可能没有时间在 window 关闭之前完成,请参阅 this post.

public override void CanClose(Action<bool> callback)
{
    if (BackTestCollection.Any(bt => bt.TestStatus == TestStatus.Running))
    {
        // Update running test.
        var cleanupTask = Task.Run(async () =>
        {
            StatusMessage = "Stopping running backtest...";
            await SaveBackTestEventsAsync(SelectedBackTest);

            // other cleanup  tasks
            // No continuation

            StatusMessage = "Disposing backtest engine...";
             if (engine != null)
                engine.Dispose();
             Log.Trace("Shutdown requested: disposed backtest engine successfully");
             callback(true);
        });
        cleanupTask.Wait();
    }
}

如果你真的需要使用延续,你也可以使用 TaskFactory.StartNew 和 TaskCreationOptions.AttachedToParent。

您可以使用类似于 WinForm Application.DoEvents 的东西,但对于 WPF,它涉及使用标志,触发您的任务,不是 Waiting ,但在循环中不断处理 UI 消息,直到您的任务完成并设置标志。 例如

if (BackTestCollection.Any(bt => bt.TestStatus == TestStatus.Running))
{
    bool done = false;
    // Update running test.
    Task.Run(async () =>
    {
        StatusMessage = "Stopping running backtest...";
        await SaveBackTestEventsAsync(SelectedBackTest);
        Log.Trace(String.Format(
            "Shutdown requested: saved backtest \"{0}\" with events",
            SelectedBackTest.Name));

        this.source = new CancellationTokenSource();
        this.token = this.source.Token;
        var filter = Builders<BsonDocument>.Filter.Eq(
            BackTestFields.ID, DocIdSerializer.Write(SelectedBackTest.Id));
        var update = Builders<BsonDocument>.Update.Set(BackTestFields.STATUS, TestStatus.Cancelled);
        IMongoDatabase database = client.GetDatabase(Constants.DatabaseMappings[Database.Backtests]);
        await MongoDataService.UpdateAsync<BsonDocument>(
            database, Constants.Backtests, filter, update, token);
        Log.Trace(String.Format(
            "Shutdown requested: updated backtest \"{0}\" status to \"Cancelled\"",
            SelectedBackTest.Name));
        StatusMessage = "Disposing backtest engine...";
        if (engine != null)
            engine.Dispose();
        Log.Trace("Shutdown requested: disposed backtest engine successfully");
        callback(true);
        done = true;
    });

    while (!done)
    {
        Application.Current.Dispatcher.Invoke(DispatcherPriority.Background,
                                new Action(delegate { }));
    }
}

这有点 hacky,但考虑到您的情况并且无法控制调用代码,这可能是您保持响应的唯一选择 UI 而不立即返回给调用者。

我尝试了async/await组合来解决这类问题。首先,我们将 sync void CanClose 转换为 async void。然后 async void 方法调用 async Task 方法来完成工作。我们必须这样做,因为在捕获异常时存在 async void 的危险。

public override async void CanClose(Action<bool> callback)
{
   await CanCloseAsync(callback);
}

public async Task CanCloseAsync(Action<bool> callback)
{
    var result1 = await DoTask1();
    if (result1)
        await DoTask2();
    callback(result1);
}

在我看来,使用这种方法有以下好处:

  • 更容易理解和理解
  • 更简单的异常处理

注:

  • 我省略了代码片段中的取消标记,如果您愿意,可以轻松添加。
  • async/await关键字存在于.net framework 4.5和c# 5.0之后