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

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 автоматически знает, что контекст вызова нужно удалить?

16
BlueRaja - Danny Pflughoeft 1 Фев 2015 в 01:11

2 ответа

Лучший ответ

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

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

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

Однако допустим, что 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

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

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

Async / await, настраиваемый awaiter и сборщик мусора

Кстати, есть еще один вид конечного автомата C #: метод с 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.

9
5 revs 23 Май 2017 в 10:26

Убедитесь, что ваши задачи всегда выполняются.

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

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

6
Stephen Cleary 31 Янв 2015 в 23:11