Не вызов Delegate.EndInvoke может вызвать утечку памяти... миф?
Об этом много дискутировали, и каждый склонен согласиться с тем, что вы всегда должны называть Delegate.EndInvoke для предотвращения утечки памяти (даже Джон Скит сказал это!).
Я всегда следил за этим руководством без вопросов, но недавно я реализовал свой собственный класс AsyncResult и увидел, что единственным ресурсом, который может протекать, является AsyncWaitHandle.
(На самом деле это действительно не утечка, потому что собственный ресурс, используемый WaitHandle, инкапсулирован в SafeHandle, у которого есть Finalizer, он добавит давление на очередь финализации сборщика мусора. Тем не менее, хорошая реализация AsyncResult будет только инициализировать AsyncWaitHandle по запросу...)
Лучший способ узнать, есть ли утечка, - это просто попробовать:
Action a = delegate { };
while (true)
a.BeginInvoke(null, null);
Я запустил это некоторое время, и память останется между 9-20 МБ.
Давайте сравним, когда вызывается Delegate.EndInvoke:
Action a = delegate { };
while (true)
a.BeginInvoke(ar => a.EndInvoke(ar), null);
С помощью этого теста память воспроизводится между 9-30 МГ, странно, а? (Вероятно, потому что для выполнения AsyncCallback требуется немного больше времени, поэтому в ThreadPool будет больше делегированных в очереди делегатов)
Как вы думаете... "Миф разорился"?
P.S. ThreadPool.QueueUserWorkItem на сто эффективнее, чем Delegate.BeginInvoke, лучше использовать его для запуска и перезапуска вызовов.
Ответы
Ответ 1
Я выполнил небольшой тест, чтобы вызвать делегата Action и выбросить в него исключение. Затем я убеждаюсь, что я не заливаю пул потоков, поддерживая только определенное количество потоков, запущенных за один раз, и заполняю пул потоков непрерывно, когда вызов на удаление завершен. Вот код:
static void Main(string[] args)
{
const int width = 2;
int currentWidth = 0;
int totalCalls = 0;
Action acc = () =>
{
try
{
Interlocked.Increment(ref totalCalls);
Interlocked.Increment(ref currentWidth);
throw new InvalidCastException("test Exception");
}
finally
{
Interlocked.Decrement(ref currentWidth);
}
};
while (true)
{
if (currentWidth < width)
{
for(int i=0;i<width;i++)
acc.BeginInvoke(null, null);
}
if (totalCalls % 1000 == 0)
Console.WriteLine("called {0:N}", totalCalls);
}
}
После того, как он пропустил около 20 минут, а более 30 миллионов звонков BeginInvoke позже, потребление памяти частного байта было постоянным (23 МБ), а также счетчиком Handle. Кажется, нет никакой утечки. Я прочитал книгу Джеффри Рихтера С# через CLR, где он утверждает, что происходит утечка памяти. По крайней мере, это похоже на то, что это не относится к .NET 3.5 SP1.
Условия тестирования:
Windows 7 x86
.NET 3.5 SP1
Intel 6600 Dual Core 2,4 ГГц
С уважением, Алоис Краус
Ответ 2
Независимо от того, протекает ли в настоящее время память, вы не должны зависеть. В будущем каркасная команда может изменить ситуацию таким образом, чтобы вызвать утечку, и потому что официальная политика "вы должны назвать EndInvoke", а затем "по дизайну".
Действительно ли вы хотите, чтобы ваше приложение внезапно начало утечку памяти в будущем, потому что вы предпочли полагаться на наблюдаемое поведение по документированным требованиям?
Ответ 3
В некоторых ситуациях BeginInvoke не нуждается в EndInvoke (особенно в обмене оконными сообщениями WinForms). Но есть определенные ситуации, когда это имеет значение - например, BeginRead и EndRead для асинхронной связи. Если вы хотите запустить Fire-and-forget BeginWrite, через некоторое время вы, вероятно, столкнетесь с серьезной проблемой памяти.
Итак, ваш один тест не может быть окончательным. Вам нужно иметь дело со многими различными типами делегатов асинхронных событий для правильного решения вашего вопроса.
Ответ 4
Рассмотрим следующий пример, который запущен на моей машине в течение нескольких минут и достиг рабочего набора в 3,5 ГБ, прежде чем я решил его убить.
Action a = delegate { throw new InvalidOperationException(); };
while (true)
a.BeginInvoke(null, null);
ПРИМЕЧАНИЕ. Обязательно запустите его без приложенного отладчика или с отключенным "break on exception throw" и "break on user-unhandled exception".
РЕДАКТИРОВАТЬ: Как отмечает Джефф, проблема с памятью здесь не является утечкой, а просто случаем подавления системы путем работы в очереди быстрее, чем ее можно обработать. В самом деле, такое же поведение можно наблюдать, заменив бросок на любую подходящую длительную операцию. И использование памяти ограничено, если мы оставляем достаточно времени между вызовами BeginInvoke.
Технически это оставляет исходный вопрос без ответа. Однако, независимо от того, может ли он вызвать утечку, не вызвав Delegate.EndInvoke - плохая идея, так как это может привести к игнорированию исключений.