当应用程序退出时,我该如何等待我的异步操作完成?

15

如果用户执行了某个操作(比如删除项目),则会立即从UI中删除该项目,并在后台线程中使用TPL从数据库中删除。问题是,如果用户在后台线程完成之前退出应用程序,则项目实际上永远不会被删除。

有没有标准方法在关闭应用程序之前等待异步操作完成?

我的异步调用看起来像这样:

if (MyObjectList.Contains(obj)) MyObjectList.Remove(obj);
Task.Factory.StartNew(() => DAL<MyEntities>.DeleteObject(obj));

更新

这是我最终采用的代码。很高兴看到它能够正常工作,但如果有改进的地方,请告诉我。我还有很多需要学习的 :)

public partial class App : Application
{
    private List<Task> _backgroundTasks = new List<Task>();

    public App()
    {
        EventSystem.Subscribe<TaskStartedMessage>((e) =>
        {
            _backgroundTasks.Add(e.Task);
        });

        EventSystem.Subscribe<TaskEndedMessage>((e) =>
        {
            if (_backgroundTasks.Contains(e.Task))
                _backgroundTasks.Remove(e.Task);
        });
    }

    protected override void OnExit(ExitEventArgs e)
    {
        Task.WaitAll(_backgroundTasks.Where(p => !p.IsCompleted).ToArray(), 30000);

        base.OnExit(e);
    }
}

在启动重要的后台任务时,我使用以下语法:

var task = Task.Factory.StartNew(() => DAL<MyEntities>.DeleteObject(obj));
EventSystem.Publish<TaskStartedMessage>(new TaskStartedMessage(task));
await task;
EventSystem.Publish<TaskEndedMessage>(new TaskEndedMessage(task));

我正在使用AsyncCTP进行await/async,以及Microsoft Prism的EventAggregator作为事件系统。


1
最终的代码看起来不错,但我会移除foreach并替换为:Task.WaitAll(_backgroundTasks.ToArray()); 我想不出你的实现会有什么问题,但我猜想如果他们实现了那个静态方法,一定是有原因的。 - Louis Kottmann
@Baboon,谢谢,我不知道还可以在任务列表上使用Task.WaitAll()方法。 - Rachel
4个回答

10

目前没有标准的方法,但由于您在此处创建了一个特定的任务,将其放入列表中并构建一些退出逻辑以等待该列表中的所有任务应该很容易。

好的,这是一个示例。未经测试和不完整的代码如下:

// untested
static class CriticalTasks
{
    static HashSet<Task> tasks = new HashSet<Task>();
    static object locker = new object();

    // when starting a Task
    public static void Add(Task t)
    {
        lock(locker)
           tasks.Add(t);
    }

    // When a Tasks completes
    public static void Remove(Task t)
    {
        lock(locker)
           tasks.Remove(t);
    }

    // Having to call Remove() is not so convenient, this is a blunt solution. 
    // call it regularly
    public static void Cleanup()
    {
        lock(locker)
           tasks.RemoveWhere(t => t.Status != TaskStatus.Running);
    }

    // from Application.Exit() or similar. 
    public static void WaitOnExit()
    {
        // filter, I'm not sure if Wait() on a canceled|completed Task would be OK
        var waitfor = tasks.Where(t => t.Status == TaskStatus.Running).ToArray();
        Task.WaitAll(waitfor, 5000);
    }
}


缺点是你需要为每个任务添加和删除代码。

如果忘记调用Remove()(例如当出现异常时),可能会导致(小型)内存泄漏。这并不是太严重的问题,相反,你可以通过定期运行Cleanup()方法来减轻代码负担,该方法使用HashSet.RemoveWhere()来删除未运行的任务,而不是将using()块加到代码中。


谢谢 :) 我喜欢看代码示例,因为它可以指引我正确的方向,而且我经常能从中学到新东西。 - Rachel
@Rachel,好的,但是我现在意识到你需要使它线程安全。我会稍后进行编辑。 - H H

0
没有一个糟糕的代码示例。答案是你根本不能这样做。如果应用程序域开始终止,您有一个有限的时间窗口来执行清理任务,然后所有这些任务都会被强制终止。您最好的行动方案是拥有某种作业控制,将工作状态排队。这将保护免受异常终止的影响。
否则,您只需在执行WaitAll并完全完成之前不要开始退出即可。

1
你能否在你的回答中添加一些描述? - benka
是的。我会添加注释。抱歉。简短的回答是,如果你想完成所有任务,就不能开始退出程序。因此,在OnExit处理程序中放置Tasks.WaitAll是一个糟糕的选择,因为一旦应用程序域决定关闭,你只有很短的时间来执行清理操作,一旦这段时间结束,应用程序将被强制终止。所以基本上我的代码在程序退出之前执行WaitAll。如果你想要绝对没有失败,那么你需要有一些作业控制系统,并具备恢复功能。 - user3524983

0

你可以存储对任务的引用,并在应用程序退出时等待它(例如在 Exit 事件上)。

你也可以创建一个普通线程,确保将 IsBackground 设置为 false(默认值)。进程不会退出,直到所有非后台线程完成工作,因此它将一直运行到结束,你必须注意不要使用任何 GUI 逻辑或确保不要释放你从该线程中需要的对象。


将其在后台线程上运行的整个目的是防止在删除对象时锁定UI。这样做只会在主线程上运行删除代码并锁定UI,不是吗? - Rachel
@Rachel:线程是否作为UI的背景与其是否作为后台线程无关。后台线程是一种应该在所有非后台线程退出时终止的线程。非后台线程将继续执行它的工作。因此,如果您在非后台线程上运行数据库逻辑,则它将独立于主UI完成其工作。 - Marcin Deptuła

0
var x = Task.Factory.StartNew(() => DAL<MyEntities>.DeleteObject(obj));

在表单关闭事件中:
while(!x.IsCompleted){hide form}

或者

if(!x.IsCompleted)
   //cancel exit

1
我强烈建议避免这种会将核心升至100%的旋转。 - Felice Pollano

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