Не закрывается процесс Excel.exe, созданный через Interop.Excel?

22 мая 2009 г.

Никак не вычищается из памяти Excel.exe? Даже делаете все по шагам из статьи Office application does not quit after automation from Visual Studio .NET client? Уже решили убивать все весящие Excel-процессы командой Kill?

Я тоже несколько раз заходил на решение этой проблемы, но никак не мог понять, почему процесс Excel.exe никак не хочет вылезать из памяти? Между тем генерация Excel или Word документов частенько требуется в различных проектах.

Вопрос на засыпку: в какой строке будет утечка памяти?

   1:  Application application = new Application {Visible = false};
   2:  Workbook workbook = application.Workbooks.Add(Type.Missing);
   3:  Worksheet sheet = (Worksheet) workbook.ActiveSheet;
   4:   
   5:  ...
   6:   
   7:  sheet.Cells[1, 1] = "columnTitle";
   8:  sheet.Cells[1, 1].EntireColumn.AutoFit();
   9:   
  10:  ...
  11:   
  12:  Marshal.ReleaseComObject(sheet);
  13:   
  14:  workbook.Close(false, Type.Missing, Type.Missing);
  15:  Marshal.ReleaseComObject(workbook);
  16:   
  17:  application.Quit();
  18:  Marshal.ReleaseComObject(application);

Те, кто прочитал Office application does not quit..., смело скажу во 2ой и буду абсолютно правы. Те, кто нашли GOTCHA: Com Interop, захотят исправить код на:

   1:  Application application = new Application {Visible = false};
   2:  Workbooks workbooks = application.Workbooks;
   3:  Workbook workbook = workbooks.Add(Type.Missing);
   4:  Worksheet sheet = (Worksheet) workbook.ActiveSheet;
   5:   
   6:  ...
   7:   
   8:  Range range = sheet.Cells;
   9:  range[1, 1] = "columnTitle";
  10:  range[1, 1].EntireColumn.AutoFit();
  11:   
  12:  Marshal.ReleaseComObject(range);
  13:   
  14:  ...
  15:   
  16:  Marshal.ReleaseComObject(sheet);
  17:   
  18:  workbook.Close(false, Type.Missing, Type.Missing);
  19:  Marshal.ReleaseComObject(workbook);
  20:   
  21:  Marshal.ReleaseComObject(workbooks);
  22:   
  23:  application.Quit();
  24:  Marshal.ReleaseComObject(application);

И все бы хорошо, только строчка 10 до сих пор "протекает". Исправить это можно так:

   1:  ...
   2:   
   3:  var currentCell = (Range)range[1, 1];
   4:  Range entireColumn = currentCell.EntireColumn;
   5:  entireColumn.AutoFit();
   6:   
   7:  Marshal.ReleaseComObject(entireColumn);
   8:  Marshal.ReleaseComObject(currentCell);
   9:   
  10:  ...

В комментариях несколько полезных советов по этой теме...

13 комментариев:

  1. Значит нормально работать с Excel.Interop и избежать утечек памяти могут только законченные маньяки-параноики :)

    ОтветитьУдалить
  2. @Shum
    Да, другая крайность запоминать все процессы до начала работы с Excel и убивать созданный процесс командой Kill =)

    ОтветитьУдалить
  3. Этот комментарий был удален администратором блога.

    ОтветитьУдалить
  4. А если использовать динамическую привязку к Excel?
    Через:
    obj.GetType().InvokeMember(name, BindingFlags.GetProperty, null, obj, null);

    ОтветитьУдалить
  5. @nikdmt
    Да, можно и так. Такой вариант я даже находил где-то на MSDN. На самом деле по синтаксису он не на много проще стандартного варианта (который привел я).

    Спасибо за дополнение!

    ОтветитьУдалить
  6. По идее все должно решаться без изращений (без ReleaseComObject) если запускать процедуру работы с Excel в отдельном Application Domain

    См. AppDomain.CreateDomain...

    Признаюсь, что с Excel не пробовал, но с другими COM компонентами помогало.

    ОтветитьУдалить
  7. @Ivan
    Да, как вариант.

    Уже получается:
    1. Следить за удалением COM объектов из памяти
    2. Использовать позднее связывание
    3. Создать COM объекты в отдельном домене

    ОтветитьУдалить
  8. Да. Забавный этот Excel. После исправлений одной программы чистил все через ReleaseComObject, приравнивал линки к null + сразу делал GC.Collect. Excel выгружается, но на всякий пожарный всетаки оставил код с убийством процесса :))

    ОтветитьУдалить
  9. Для истинных ценителей всей убогости Excel.Interop предлагаю 100% гарантированный и безопасный способ борьбы с висящими зомби-процессами:

    [DllImport("user32.dll", SetLastError = true)]
    static extern uint GetWindowThreadProcessId(int hWnd, ref int lpdwProcessId);
    .......
    application = new Application {Visible = false, DisplayAlerts = false};
    int excelProcessId = -1;
    GetWindowThreadProcessId(application.Hwnd, ref excelProcessId);
    //
    // Выполняем операции
    // и выходим штатным методом
    // после этого для верности пытаемся прибить процесс
    try
    {
    var process = Process.GetProcessById(excelProcessId);
    process.Kill();
    }

    Таким образом вы никогда не прибьёте чужой Excel процесс, но свой прибьёте со 100% гарантией.

    Осталось добавить сюда запуск в отдельном процессе с таймаутом ожидания (иначе на простой workbook.Close() на сервере может вылезти диалоговое окошко и вы никогда об этом не узнаете) - и всё будет по фэн-шую :)

    ОтветитьУдалить
  10. Да, Денис, ты прокачался круче всех =)

    ОтветитьУдалить
  11. Нет предела совершенству :) Новый вариант БЕЗ использования не фэн-шуйного DllImport'а - прибиваем процесс по его HWND:

    Process.GetProcesses().Where(p => p.MainWindowHandle.ToInt32() == applicationHwnd).ToList().ForEach(process => process.Kill());

    ОтветитьУдалить
  12. после строки 10 у меня только 1 строка:
    application = null;
    для того, чтобы отвязать указатель =)
    после закрытия программы и эксэля процесс умирает

    ОтветитьУдалить
  13. Эта строка абсолютно бесполезна
    application = null;
    Потому что все .NET объекты умирают, когда выходят из зоны видимости. В памяти остается именно COM объект, т.к. не все ссылки на него уничтожены.
    Если ты будешь использовать сложные конструкции типа
    sheet.Cells[1, 1].EntireColumn.AutoFit();
    то excel.exe скорее всего останется висеть в памяти.

    ОтветитьУдалить

Моя книга «Антихрупкость в IT»

Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...