链接任务。WhenAll()

3

我想出了一段代码,将多个调用Task.WhenAll()链接在一起。我认为这样可以实现目的,但看起来有点奇怪。它的目的是在关闭服务之前允许所有Tasks完成。伪代码省略了一些循环、方法声明等...

//initialize once on startup
Task _completion = Task.FromResult(0); 

//Every minute a timer fires and we start some tasks     
// and then chain them into the existing Task
var newTasks = Enumerable.Range(0, 10).Select(_ => Task.Factory.StartNew(() => {/* long running stuff here */})).ToArray();
_completion = Task.WhenAll(_completion, Task.WhenAll(newTasks));

//At some point a shutdown will be requested, and we can just wait for the one Task to complete
_completion.Wait();

这是一个不好的主意吗?我会一直持有对每个任务的引用,导致它们永远无法被垃圾回收,或者导致某些内部数组变得庞大,或者发生其他可怕的事情吗?
对我来说,反复从Task.WhenAll()获取结果并将其反馈到Task.WhenAll()感觉有点奇怪。我查看了Task.WhenAll()的源代码,我没有看到任何表明这可能是个问题的地方。但我肯定不是这个主题的专家。

看起来 Task.WaitAll(tasks) 在这里更适合你。它会阻塞当前的执行,直到所有给定的任务都完成。 - abatishchev
在这个简化的例子中,阻塞可能更容易,但在实际情况下,我不想阻塞执行。我可能会有几个批次同时运行。此外,我需要让主线程空闲以响应关闭请求。 - Brian Reischl
2个回答

5
我会一直持有所有任务的引用,这样它们就永远无法被垃圾回收?
当所有任务完成时,Task.WhenAll会释放所有任务的内存。这意味着对于未完成的任何任务,都会保留所有同一“批次”的其他任务以及每个“上层”批次的内存。如果您的批处理大小特别大,并且完成时间差异很大,那可能是个问题。如果你没有这个问题,那么你的代码应该没问题。
幸运的是,这个问题可以很容易地进行优化。您可以使用一个类,将每个活动任务添加到任务集中,然后在任务完成时删除每个任务。然后,您可以轻松地等待每个当前活动任务。这确保已完成的任务不再保留对它们的引用。这不仅意味着不会比必要时间更长地保留旧类,而且将“保留所有活动任务”的逻辑分离到一个地方,从而简化了主应用程序中的逻辑。除了内存优化之外,这也可以提高代码的清晰度。
public class ActiveTaskTracker
{
    private HashSet<Task> tasks = new HashSet<Task>();
    public void Add(Task task)
    {
        if (!task.IsCompleted)//short circuit as an optimization
        {
            lock (tasks)
                tasks.Add(task);
            task.ContinueWith(t => { lock (tasks)tasks.Remove(task); });
        }
    }
    public Task WaitAll()
    {
        lock (tasks)
            return Task.WhenAll(tasks.ToArray());
    }
}

阅读源代码,看起来我们最终会进入WhenAllPromise.Invoke(),该方法将在第6155行将已完成任务的引用设置为null。或者我理解有误吗?http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs#6155 - Brian Reischl
如果您使用某种同步集合,这个类是否可以改进?那么您就不必显式地进行锁定。 - NeddySpaghetti
@NedStoyanov 你会推荐哪种同步集合吗? - Servy
ConcurrentQueue 怎么样? - NeddySpaghetti
@NedStoyanov 你打算如何从该集合中删除每个已完成的任务? - Servy
显示剩余3条评论

0
我会一直持有每个任务的引用,以至于它们永远不会被垃圾回收吗?
这取决于情况。
单个Task.WhenAll(X)将在X中的每个元素完成时立即释放对所有任务的引用1。换句话说,如果你有Task.WhenAll(A, Task.WhenAll(B)),则在B完成后,即使A没有完成,也不会保留对B的引用。因此,只要更深层次的任务继续完成,它们就应该继续被删除。
但是请注意,如果您有一个非常深的单个任务被“卡住”(即永远不会完成),那么您将得到一个无限增长的链。
您添加到链中的方式(例如chain = Task.WhenAll(chain, Task.WhenAll(newTasks)))可以缓解这个问题,因为即使chain本身被卡住并增长,内部的Task.WhenAll()仍然可以释放任务。
另一方面,Servy发布的答案中的代码不会遇到这个问题。

1来源于参考来源(Task.cs)

private sealed class WhenAllPromise : Task<VoidTaskResult>, ITaskCompletionAction
{
    public void Invoke(Task completedTask)
    {
        ...
        // Decrement the count, and only continue to complete the promise if we're the last one.
        if (Interlocked.Decrement(ref m_count) == 0)
        {
            ...
            for (int i = 0; i < m_tasks.Length; i++)
            {
                ...
                // Regardless of completion state, if the task has its debug bit set, transfer it to the
                // WhenAll task.  We must do this before we complete the task.
                if (task.IsWaitNotificationEnabled) this.SetNotificationForWaitCompletion(enabled: true);
                else m_tasks[i] = null; // avoid holding onto tasks unnecessarily
            }
        }
    }
}

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