Безопасно ли оставлять задачу без ссылки - PullRequest
2 голосов
/ 29 февраля 2012

Безопасно ли оставлять задачу без ссылки, если я точно знаю, что она не выдает исключение? Будет ли GC ждать, пока задача не будет завершена, прежде чем заберет ее?

Вот пример моего метода, который преобразует массив задач в одну задачу, которая завершается (отменяется или не выполняется), когда все задачи завершаются. Мое приложение завершается с ошибкой без наблюдения задачи (зарегистрировав Task.Id в любом месте, где я использую задачи, я обнаружил, что задача, которая осталась ненаблюдаемой, это та, которая поставляется этому методу или, по крайней мере, имеет тот же идентификатор). Я понятия не имею, почему это происходит, за исключением того, что сборщик мусора собирает задачу, возвращенную из Task.Factory.ContinueWhenAll, не ожидая, когда она будет завершена, как таковая, она также может собирать все мои задачи из массива, оставленного без ссылки, и если есть хотя бы одна неудачная задача это приведет к задаче ненаблюдаемого исключения. Звучит безумно, но я не вижу другого объяснения тому, что происходит. Так возможно ли это?

        public static Task ToWhenAllTask(this Task[] tasks, bool cancelIfAnyCanceled = true)
    {
        if (tasks != null && tasks.Length == 0)
            throw new ArgumentException();

        var tcs = new TaskCompletionSource<object>();

        Task.Factory.ContinueWhenAll(tasks, ts => {
            try
            {
                List<Exception> errors = null;
                bool canceled = false;

                foreach (Task task in ts)
                {
                    AggregateException ex = task.Exception;

                    if (ex != null)
                    {
                        if (errors == null)
                            errors = new List<Exception>();

                        errors.Add(ex.Flatten());
                    }

                    if (task.IsCanceled)
                        canceled = true;
                }

                if (errors != null)
                    tcs.TrySetException(errors);
                else if (cancelIfAnyCanceled && canceled)
                    tcs.TrySetCanceled();
                else
                    tcs.TrySetResult(null);
            }
            catch(Exception ex)
            {

                // there is nothing to fail in this method but just in case
                tcs.TrySetException(ex);
            }

        }, TaskContinuationOptions.ExecuteSynchronously);

        return tcs.Task;
    }

PS. Честно говоря, я думал, что до тех пор, пока задача не будет завершена, TaskScheduller будет содержать ссылку на нее (и в моем случае задача продолжения также содержит ссылку на массив задач). Поэтому GC не может собрать задачу продолжения и все задачи из массива, пока все они не завершатся.

1 Ответ

2 голосов
/ 21 марта 2012

Наконец-то я нашел причину своей проблемы. И прямой ответ на мой вопрос, размещенный в первых строках: да, безопасно оставить задачу без ссылки, если вы точно знаете, что она не завершится с ошибкой. И да, задачи не будут собираться GC, пока они не завершат НО, только если они начались (то есть они были запланированы в TaskScheduler)

Слово «НО» заглавными буквами означает, что может возникнуть проблема в очень конкретном случае: если вы оставили экземпляр Task в запланированном состоянии и потеряли все ссылки на него.

Вот конкретный пример (на самом деле, что случилось со мной, когда я разместил здесь вопрос), если вы выполняете ContinueWhenAll для нескольких задач, одна из которых завершилась ошибкой, и хотя бы одна не запланирована (а остальные выполнены если они есть), и вы теряете ссылки на все эти задачи, а также не сохраняете ссылку на ту, что была возвращена ContinueWhenAll, то все они будут собраны GC при следующем сборе мусора. И эта невыполненная задача из тех, что были переданы в ContinueWhenAll, приведет к исключению необнаруженной задачи.

Не запланированное задание выше означает, что оно было создано одним из способов:

  1. Вызвана только новая задача (...) (без дальнейшего вызова Start метод)
  2. он был создан TaskCompleteionSource, но не установлен в завершение, состояние сбоя или отменено.

Такое поведение выглядит согласованным с точки зрения TPL. Просто потому, что вы не должны предоставлять задачи, которые никогда не будут запланированы для метода ContinueWhenAll и ждать вечно. Таким образом, на самом деле, если ни одна из других задач, переданных в ContinueWhenAll, не будет выполнена, продолжение никогда не произойдет, а не возникнет исключение необязательной задачи. Вот и все!

...