Скажем, у меня есть следующий класс:
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 автоматически знает, что контекст вызова нужно удалить?
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
.
Убедитесь, что ваши задачи всегда выполняются.
В обычном случае «Другой код, который вызывает SetResult» где-то регистрируется как обратный вызов. Например, если он использует неуправляемый перекрывающийся ввод-вывод, то этот метод обратного вызова является корнем сборщика мусора. Затем этот обратный вызов явно поддерживает _someTask
в рабочем состоянии, что поддерживает его Task
в рабочем состоянии, что сохраняет работоспособность делегата для //Some more code
.
Если «Другой код, который вызывает SetResult», не (прямо или косвенно) зарегистрирован как обратный вызов, то я не думаю , что произойдет утечка. Обратите внимание, что это не поддерживаемый вариант использования, поэтому это не гарантируется. Но я создал тест профилирования памяти, используя код из вашего вопроса, и, похоже, он не протекает.
Похожие вопросы
Связанные вопросы
Новые вопросы
c#
C# (произносится как «see Sharp») — это высокоуровневый мультипарадигменный язык программирования со статической типизацией, разработанный Microsoft. Код C# обычно нацелен на семейство инструментов и сред выполнения Microsoft .NET, которое включает в себя .NET, .NET Framework, .NET MAUI и Xamarin среди прочих. Используйте этот тег для ответов на вопросы о коде, написанном на C#, или о формальной спецификации C#.