Подтвердить что ты не робот

Что происходит с задачами, которые никогда не выполняются? Правильно ли они расположены?

Скажем, у меня есть следующий класс:

class SomeClass
{
    private TaskCompletionSource<string> _someTask;

    public Task<string> WaitForThing()
    {
        _someTask = new TaskCompletionSource<string>();
        return _someTask.Task;
    }

    //Other code which calls _someTask.SetResult(..);
}

Затем в другом месте я называю

//Some code..
await someClassInstance.WaitForThing();
//Some more code

//Some more code не будет вызываться до вызова _someTask.SetResult(..). Зрительный контекст где-то ждет где-то в памяти.

Однако, скажем, SetResult(..) никогда не вызывается, а someClassInstance перестает ссылаться и собирается мусор. Это создает утечку памяти? Или .Net автоматически-волшебным образом знает, что контекст вызова должен быть удален?

4b9b3361

Ответ 1

Обновлен, хороший момент by @SriramSakthivel, оказалось, что я уже ответил на очень похожий вопрос:

Почему GC собирает мой объект, когда у меня есть ссылка на него?

Итак, я отмечаю это как вики сообщества.

Однако, пусть SetResult (..) никогда не вызывается и someClassInstance перестает ссылаться и собирается мусор. Это создает утечку памяти? Или .Net автоматически узнает контекст вызова должен быть удален?

Если по вызову-контексту вы имеете в виду объект автомата, созданный компилятором (который представляет состояние метода async), то да, он действительно будет завершен.

Пример:

static void Main(string[] args)
{
    var task = TestSomethingAsync();
    Console.WriteLine("Press enter to GC");
    Console.ReadLine();
    GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
    GC.WaitForFullGCComplete();
    GC.WaitForPendingFinalizers();
    Console.WriteLine("Press enter to exit");
    Console.ReadLine();
}

static async Task TestSomethingAsync()
{
    using (var something = new SomeDisposable())
    {
        await something.WaitForThingAsync();
    }
}

class SomeDisposable : IDisposable
{
    readonly TaskCompletionSource<string> _tcs = new TaskCompletionSource<string>();

    ~SomeDisposable()
    {
        Console.WriteLine("~SomeDisposable");
    }

    public Task<string> WaitForThingAsync()
    {
        return _tcs.Task;
    }

    public void Dispose()
    {
        Console.WriteLine("SomeDisposable.Dispose");
        GC.SuppressFinalize(this);
    }
}

Вывод:

Press enter to GC

~SomeDisposable
Press enter to exit

IMO, это поведение логично, но все же может быть немного неожиданным, что something завершается, несмотря на то, что область using для него никогда не заканчивалась (и, следовательно, ее SomeDisposable.Dispose никогда не вызывалась) и что Task, возвращаемый TestSomethingAsync, все еще жив и указан в Main.

Это может привести к некоторым неясным ошибкам при кодировании асинхронных данных на системном уровне. Очень важно использовать GCHandle.Alloc(callback) для любых обратных вызовов взаимодействия с ОС, на которые не ссылаются внешние методы async. Выполнение GC.KeepAlive(callback) только в конце метода async неэффективно. Я писал об этом подробнее:

Async/await, пользовательский awaiter и сборщик мусора

На боковой заметке существует еще один тип конечного автомата С#: метод с return yield. Интересно, что наряду с IEnumerable или IEnumerator он также реализует IDisposable. Вызов его Dispose приведет к отключению любых операторов using и finally (даже в случае неполной перечислимой последовательности):

static IEnumerator SomethingEnumerable()
{
    using (var disposable = new SomeDisposable())
    {
        try
        {
            Console.WriteLine("Step 1");
            yield return null;
            Console.WriteLine("Step 2");
            yield return null;
            Console.WriteLine("Step 3");
            yield return null;
        }
        finally
        {
            Console.WriteLine("Finally");
        }
    }
}
// ...
var something = SomethingEnumerable();
something.MoveNext(); // prints "Step 1"
var disposable = (IDisposable)something;
disposable.Dispose(); // prints "Finally", "SomeDisposable.Dispose"

В отличие от этого, при методах async нет прямого способа управления нераскрытием using и finally.

Ответ 2

Вы должны убедиться, что ваши задачи всегда завершены.

В обычном случае "Другой код, который вызывает SetResult" где-то зарегистрирован как обратный вызов. Например, если он использует неуправляемый перекрывающийся ввод-вывод, то этот метод обратного вызова является корнем GC. Тогда этот обратный вызов явно сохраняет _someTask alive, который сохраняет его Task вживую, что сохраняет делегат для //Some more code вживую.

Если "Другой код, который вызывает SetResult", не является (прямо или косвенно) зарегистрированным в качестве обратного вызова, то я не думаю, что будет утечка. Обратите внимание, что это не поддерживается, поэтому это не гарантируется. Но я создал тест профилирования памяти, используя код в вашем вопросе, и он, похоже, не течет.