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

时间:2015-08-23 14:07:21

标签: c# asynchronous task semaphore blocking

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

如何在不过早返回调用者的情况下运行清理代码?

感谢您的时间。

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

3 个答案:

答案 0 :(得分:8)

我认为你没有多少选择阻止回报。但是,尽管UI线程被锁定,您的更新仍应继续运行。我不会使用ManualResetEventSlim,而是使用简单的wait()和没有延续的单个任务。原因是默认情况下,Task.Run会阻止子任务(您的继续)附加到父级,因此在窗口关闭之前您的继续可能没有时间完成,请参阅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 () =>
        {
            Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { StatusMessage.Text = "Stopping running backtest..."; }));
            await SaveBackTestEventsAsync(SelectedBackTest);

            // other cleanup  tasks
            // No continuation

            Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { StatusMessage.Text = "Disposing backtest engine..."; }));
            if (engine != null)
                engine.Dispose();
            Log.Trace("Shutdown requested: disposed backtest engine successfully");
            callback(true);
        });
        while (!cleanupTask.IsCompleted)
        {
            Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { }));
        }
    }
}

如果你真的需要使用continuation,你也可以将TaskFactory.StartNew与TaskCreationOptions.AttachedToParent一起使用。

编辑:我将我的答案与@Saeb Amini结合起来,这有点像黑客攻击,但你保留了一些UI响应能力。

编辑2:这是解决方案的示例演示(使用新的WPF项目测试):

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    protected override void OnClosing(CancelEventArgs e)
    {
        var dispatcher = Application.Current.Dispatcher;
        var cleanupTask = Task.Run(
            async () =>
            {
                dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate {StatusMessage.Text = "Stopping running backtest..."; }));
                await Task.Delay(2000);
                dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { StatusMessage.Text = "Disposing backtest engine..."; }));
                await Task.Delay(2000);
            });

        while (!cleanupTask.IsCompleted)
        {
            dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { }));
        }
    }
}

答案 1 :(得分:4)

您可以使用与WinForm Application.DoEvents类似的内容,但对于WPF,它涉及使用标记,解雇您的任务, Wait为此,但是在循环中不断处理UI消息,直到任务完成并设置标志。 e.g

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。

答案 2 :(得分:0)

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

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
  • 之后