Не вызов 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 - плохая идея, так как это может привести к игнорированию исключений.