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

本文关键字:UI 线程 任务 等待 | 更新日期: 2023-09-27 18:09:15

我有一个相当复杂的WPF应用程序(很像VS2013)有IDocumentsITools停靠在应用程序的主外壳内。其中一个Tools需要在主窗口关闭时安全关闭,以避免进入"坏"状态。所以我使用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,这显然会在我更新后台[线程池]线程上的数据库之前返回CanClose调用者。为了防止返回,直到我完成了我的更新,我试图阻止返回,但这冻结了UI线程,并防止任何事情发生。

如何让我的清理代码运行而不过早返回调用者?

感谢您的宝贵时间。


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

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

我认为你除了阻止返回之外别无选择。然而,尽管UI线程被锁定,你的更新仍然应该运行。我不会使用ManualResetEventSlim,而只是一个简单的wait()和一个没有延续的任务。默认情况下,原因是任务。Run可以防止子任务(你的延续)被附加到父任务上,所以你的延续可能没有时间在窗口关闭之前完成,参见这篇文章。

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 with TaskCreationOptions。attachedtopparent

你可以使用类似于WinForm的Application.DoEvents的东西,但对于WPF,它涉及到使用一个标志,触发你的任务,不是 Wait为它,但在一个循环中持续处理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 { }));
    }
}

这是有点粗糙,但考虑到您的情况和无法控制调用代码,它可能是您唯一的选择,以维护一个响应UI,而不立即返回到调用者。

我尝试了async/await组合来解决这种问题。首先,我们将同步void转换为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之后存在