【個案】某支開啟Excel進行作業的.NET排程程式,定期排程執行時遇到錯誤,留下一堆無主excel.exe。

用以下程式示範:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Office.Interop.Excel;
 
namespace RunExcelBadWay
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Application excel = new Application();
                var wrkBook = excel.Workbooks.Add();
                var sheet = wrkBook.Sheets.Add();
                sheet.Cells(1, 1).Value = "ABC";
                wrkBook.SaveAs(@"d:\" + Guid.NewGuid() + ".xlsx");
                wrkBook.Close();
                excel.Workbooks.Close();
                excel.Quit();
                Console.WriteLine("Done");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error:" + ex.Message);
            }
        }
    }
}

程式貌似正常,執行結果無誤。但做個小實驗,把存檔路徑改成wrkBook.SaveAs(@"d:\PathNotThere\" + Guid.NewGuid() + ".xlsx");,故意指向不存在的目錄,接著連續執行程式12次,打開Task Manager:

哦哦,冒出12個桌面看不到摸不著的背景EXCEL.EXE,記憶體用量卻持續跳動,是會繼續吃CPU吃RAM的僵屍嗎?

Excel屬於Unmanaged程式,所佔用資源不會在.NET程式結束時自動回收。過去處理其他如FileStream、DbConnection等Unmanaged資源,用using即可搞定,但Excel Application未實做IDisposable介面,無法用using打發。在處理上,我們需要加入finally區段明確清理及關閉Excel,確保任何情況下Excel程式都會確實關閉釋放資源。更進一步,可參考MSDN Code Sample建議:

... Clean up the unmanaged COM resource. To get Excel terminated rightly, we need to call Marshal.FinalReleaseComObject() on each COM object we used ...

意思是程式過程使用過的所有COM+物件,包含Workbook、Worksheet、Range、Cell(註: 連Cell都要,挺麻煩的)... 結束前都建議使用Marshal.FinalReleaseComObject()清掉COM+物件的Reference Counter,確保Excel程式能順利關閉。

修改的範例如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Office.Interop.Excel;
 
namespace RunExcel
{
    class Program
    {
        static void Main(string[] args)
        {
            Application excel = null;
            _Workbook wrkBook = null;
            dynamic sheet = null;
            try
            {
                excel = new Application();
                wrkBook = excel.Workbooks.Add();
                sheet = wrkBook.Sheets.Add();
                sheet.Cells(1, 1).Value = "ABC";
                wrkBook.SaveAs(@"d:\PathNotThere\" + Guid.NewGuid() + ".xlsx");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error:" + ex.Message);
            }
            finally
            {
              //注意: Excel是Unmanaged程式,要妥善結束才能乾淨不留痕跡
                //否則,很容易留下一堆excel.exe在記憶體中
                //所有用過的COM+物件都要使用Marshal.FinalReleaseComObject清掉
                //COM+物件的Reference Counter,以利結束物件回收記憶體
                if (sheet != null)
                {
                    Marshal.FinalReleaseComObject(sheet);
                }
                if (wrkBook != null)
                {
                    wrkBook.Close(false); //忽略尚未存檔內容,避免跳出提示卡住
                    Marshal.FinalReleaseComObject(wrkBook);
                }
                if (excel != null)
                {
                    excel.Workbooks.Close();
                    excel.Quit();
                    Marshal.FinalReleaseComObject(excel);
                }
            }
            Console.WriteLine("Done");
        }
    }
}

由於必須逐一釋放用過的Excel物件,感覺頗麻煩,如果沒有特別需求,我會建議改用OpenXML SDK、EPPlus、ClosedXML等純.NET程式庫[參考12]更輕巧單純些。


Comments

# by Ho.Chun

請問 Microsoft.Office.Interop Package 可以直接免費使用嗎 ?

# by Jeffrey

toHo.Chun,它只是串接 Office COM+ 的程式介面,主機要安裝 Office 軟體才能使用。

Post a comment