简体   繁体   English

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

[英]Wait for Task to Complete without Blocking UI Thread

I have a fairly complex WPF application that (much like VS2013) has IDocuments and ITools docked within the main shell of the application.我有一个相当复杂的 WPF 应用程序(很像 VS2013)在应用程序的主 shell 中停靠了IDocumentsITools One of these Tools needs to be shutdown safely when the main Window is closed to avoid getting into a "bad" state.当主窗口关闭时,需要安全关闭这些Tools之一,以避免进入“坏”状态。 So I use Caliburn Micro's public override void CanClose(Action<bool> callback) method to perform some database updates etc. The problem I have is all of the update code in this method uses MongoDB Driver 2.0 and this stuff is async .所以我使用 Caliburn Micro 的public override void CanClose(Action<bool> callback)方法来执行一些数据库更新等。我public override void CanClose(Action<bool> callback)的问题是这个方法中的所有更新代码都使用 MongoDB Driver 2.0 而这些东西是async Some code;一些代码; currently I am attempting to perform目前我正在尝试执行

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

Now, to start with I did not have the ManualResetEventSlim and this would obviously return to the CanClose caller before I updated my database on the background [thread-pool] thread.现在,首先我没有ManualResetEventSlim ,这显然会在我更新后台 [thread-pool] 线程上的数据库之前返回到CanClose调用者。 In an attempt to prevent the return until I have finished my updates I tried to block the return, but this freezes the UI thread and prevents anything from happening.为了在我完成更新之前阻止返回,我试图阻止返回,但这会冻结 UI 线程并阻止任何事情发生。

How can I get my clean-up code to run without returning to the caller too early?如何在不过早返回调用者的情况下运行我的清理代码?

Thank for your time.感谢您的时间。


Note, I cannot override the OnClose method using async signature as the calling code would not await it (I have no control over this).请注意,我无法使用异步签名覆盖OnClose方法,因为调用代码不会等待它(我无法控制它)。

I don't think you have much choice than to block the return.我认为除了阻止返回之外,您别无选择。 However your updates should still run despite the UI thread being locked.但是,尽管 UI 线程被锁定,您的更新仍应运行。 I wouldn't use a ManualResetEventSlim, but just a simple wait() and a single task without a continuation.我不会使用 ManualResetEventSlim,而只是一个简单的 wait() 和一个没有继续的任务。 The reason for that is by default Task.Run prevents the child task (your continuation) from being attached to the parent and so your continuation may not have time to complete before the window closes, see this post .原因是默认情况下 Task.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();
    }
}

You can also use TaskFactory.StartNew with TaskCreationOptions.AttachedToParent if you really need to use a continuation.如果您确实需要使用延续,您还可以将 TaskFactory.StartNew 与 TaskCreationOptions.AttachedToParent 一起使用。

You can use something similar to WinForm's Application.DoEvents but for WPF, it involves using a flag, firing your task, not Wait ing for it, but continiously processing UI messages in a loop until your task is done and sets the flag.您可以使用类似于 WinForm 的Application.DoEvents东西,但对于 WPF,它涉及使用标志,触发您的任务,而不是Wait它,而是在循环中不断处理 UI 消息,直到您的任务完成并设置标志。 eg :例如

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

It's a bit hacky, but given your situation and no control over the calling code, it might be your only option to maintain a responsive UI without immediately returning to the caller.这有点 hacky,但考虑到您的情况并且无法控制调用代码,这可能是您保持响应式 UI 而不立即返回给调用者的唯一选择。

I tried the async/await combination to resolve this kind of problem.我尝试了 async/await 组合来解决这种问题。 First we convert the sync void CanClose to async void.首先我们将sync void CanClose 转换为async void。 Then the async void method calls the async Task method to do the work.然后 async void 方法调用 async Task 方法来完成工作。 We have to do this because the danger of async void when catching exceptions.我们必须这样做,因为在捕获异常时存在 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);
}

In my opinion, there are benefits of using this approach:在我看来,使用这种方法有以下好处:

  • easier to follow and understand更容易理解和理解
  • easier exception handling更简单的异常处理

Note:笔记:

  • I omitted the cancellation token in the code snippet, which can be added easily if you want to.我在代码片段中省略了取消标记,如果您愿意,可以轻松添加。
  • async/await keywords exist after .net framework 4.5 and c# 5.0 .net framework 4.5 和 c# 5.0 之后存在 async/await 关键字

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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