[C#] Excel interop 개체를 올바르게 정리하려면 어떻게합니까?

C # ( ApplicationClass) 에서 Excel interop을 사용하고 finally 절에 다음 코드를 배치했습니다.

while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();

이러한 종류의 작업이지만 Excel.exeExcel을 닫은 후에도 프로세스가 백그라운드에 있습니다. 내 응용 프로그램을 수동으로 닫은 후에 만 ​​해제됩니다.

내가 뭘 잘못하고 있거나 interop 객체를 올바르게 폐기 할 수있는 대안이 있습니까?



답변

응용 프로그램이 여전히 COM 개체에 대한 참조를 보유하고 있으므로 Excel이 종료되지 않습니다.

변수에 할당하지 않고 COM 개체의 구성원을 하나 이상 호출한다고 생각합니다.

나에게 그것은 변수에 할당하지 않고 직접 사용한 excelApp.Worksheets 객체 였습니다 .

Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);

내부적으로 C #에서 Worksheets COM 개체에 대한 래퍼를 만들지 몰랐습니다.이 코드는 알지 못했기 때문에 내 코드로 릴리스되지 않았으며 Excel이 언로드되지 않은 원인이었습니다.

이 페이지 에서 내 문제에 대한 해결책을 찾았 으며 C #에서 COM 개체 사용에 대한 훌륭한 규칙이 있습니다.

COM 객체에 두 개의 점을 사용하지 마십시오.


따라서이 지식으로 위의 작업을 수행하는 올바른 방법은 다음과 같습니다.

Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);

포스트 모트 업데이트 :

나는 모든 독자들이 한스 패넌트 (Hans Passant)가이 답변을 매우주의 깊게 읽길 바란다. 나는 함정과 다른 많은 개발자들이 우연히 발견 한 함정을 설명하고있다. 몇 년 전에이 답변을 썼을 때 디버거가 가비지 수집기에 미치는 영향을 알지 못하고 잘못된 결론을 이끌어 냈습니다. 나는 역사의 이익을 위해 변경되지 않은 내 대답을 유지하지만,이 링크를 읽어주십시오 하지 않는다 “두 점”의 길을 갈 : 이해 가비지 수집을 .NET에서청소 엑셀 Interop를 최대는 IDisposable와 개체


답변

실제로 Excel 응용 프로그램 개체를 깨끗하게 해제 할 수 있지만주의해야합니다.

액세스 한 모든 COM 개체에 대해 명명 된 참조를 유지 관리하고 명시 적으로 해제하는 데 대한 조언 Marshal.FinalReleaseComObject()은 이론 상으로는 정확하지만 불행히도 실제로 관리하기는 매우 어렵습니다. 어느 곳에서나 미끄러 져서 “두 개의 점”을 사용하거나 for each루프 또는 다른 유사한 종류의 명령을 통해 셀을 반복하는 경우 참조되지 않은 COM 개체가 있고 중단 될 위험이 있습니다. 이 경우 코드에서 원인을 찾을 수있는 방법이 없습니다. 모든 코드를 눈으로 검토하고 큰 프로젝트에서 거의 불가능한 작업 인 원인을 찾아야합니다.

좋은 소식은 실제로 사용하는 모든 COM 개체에 대한 명명 된 변수 참조를 유지할 필요가 없다는 것입니다. 대신 참조를 보유하지 않은 모든 (보통 작은) 객체를 호출 GC.Collect()한 다음 GC.WaitForPendingFinalizers()해제 한 다음 명명 된 변수 참조를 보유하는 객체를 명시 적으로 해제하십시오.

또한 범위 개체를 먼저 시작한 다음 워크 시트, 통합 문서 및 마지막으로 Excel 응용 프로그램 개체와 같이 명명 된 참조를 역순으로 해제해야합니다.

예를 들어, 이름이 Range 개체 변수, 이름 xlRng이 워크 시트 변수, 이름 xlSheet이 통합 문서 변수 xlBook및 이름 이 Excel 응용 프로그램 변수 인 xlApp경우 정리 코드는 다음과 같습니다.

// Cleanup
GC.Collect();
GC.WaitForPendingFinalizers();

Marshal.FinalReleaseComObject(xlRng);
Marshal.FinalReleaseComObject(xlSheet);

xlBook.Close(Type.Missing, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlBook);

xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);

대부분의 코드 예제에서 .NET에서 COM 개체를 정리하는 방법 은 다음 GC.Collect()과 같이 두 번 GC.WaitForPendingFinalizers()호출됩니다.

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();

그러나 최종 오브젝트를 사용하여 최종 오브젝트에서 전체 오브젝트 그래프가 승격되도록하는 종료자를 사용하는 Visual Studio Tools for Office (VSTO)를 사용하지 않는 한 이는 필요하지 않습니다. 이러한 가비지 는 다음 가비지 콜렉션 까지 해제되지 않습니다 . 당신이 VSTO를 사용하지 않는 경우, 당신은 전화를 할 수 있어야 GC.Collect()하고 GC.WaitForPendingFinalizers()한 번만.

나는 명시 적으로 전화하는 GC.Collect()것이 no-no 라는 것을 알고 있습니다 (그리고 확실히 두 번하는 것은 매우 고통스럽게 들립니다). 정상적인 작업을 통해 참조를 보유하지 않은 숨겨진 객체를 생성하므로을 호출하는 것 이외의 다른 방법으로는 풀 수 없습니다 GC.Collect().

이것은 복잡한 주제이지만, 실제로 모든 것이 있습니다. 정리 절차를 위해이 템플릿을 설정하면 래퍼 등이 없어도 정상적으로 코딩 할 수 있습니다. 🙂

여기에 대한 자습서가 있습니다.

VB.Net / COM Interop을 사용하여 Office 프로그램 자동화

VB.NET 용으로 작성되었지만 그 말로 설명하지 마십시오. 원리는 C #을 사용할 때와 정확히 동일합니다.


답변

서문 : 제 대답에는 두 가지 해결책이 포함되어 있으므로 읽을 때주의해야합니다.

Excel 인스턴스를 언로드하는 방법에 대한 여러 가지 방법과 조언이 있습니다.

  • Marshal.FinalReleaseComObject ()를 사용하여 모든 com 객체를 명시 적으로 해제 (암시 적으로 생성 된 com 객체를 잊지 않음). 생성 된 모든 com 개체를 해제하려면 여기에 언급 된 2 개의 점 규칙을 사용할 수 있습니다.
    Excel interop 개체를 올바르게 정리하려면 어떻게해야합니까?

  • CLR 릴리스를 사용하지 않는 comobjects로 만들기 위해 GC.Collect () 및 GC.WaitForPendingFinalizers () 호출 * (실제로 작동합니다. 자세한 내용은 두 번째 솔루션 참조)

  • COM 서버 응용 프로그램을 확인하면 사용자가 응답 할 때까지 기다리는 메시지 상자가 표시 될 수 있습니다 (Excel이 닫히지 못할 지 확실하지 않지만 몇 번 들었습니다)

  • 기본 Excel 창으로 WM_CLOSE 메시지 보내기

  • 별도의 AppDomain에서 Excel과 함께 작동하는 기능을 실행합니다. 일부 사람들은 AppDomain이 언로드 될 때 Excel 인스턴스가 종료 될 것이라고 생각합니다.

  • 엑셀 인터로 핑 코드가 시작된 후 인스턴스화 된 모든 엑셀 인스턴스를 종료합니다.

그러나! 때로는 이러한 모든 옵션이 도움이되지 않거나 적절하지 않을 수 있습니다!

예를 들어, 어제 Excel에서 작동하는 내 함수 중 하나에서 Excel이 함수가 끝난 후에도 계속 실행된다는 것을 알았습니다. 나는 모든 것을 시도했다! 나는 전체 기능을 10 번 철저히 점검하고 Marshal.FinalReleaseComObject ()를 모두 추가했습니다! 또한 GC.Collect () 및 GC.WaitForPendingFinalizers ()가있었습니다. 숨겨진 메시지 상자를 확인했습니다. 기본 Excel 창에 WM_CLOSE 메시지를 보내려고했습니다. 별도의 AppDomain에서 내 기능을 실행하고 해당 도메인을 언로드했습니다. 아무것도 도와주지 않았다! Excel과 함께 작동하는 함수를 실행하는 동안 사용자가 다른 Excel 인스턴스를 수동으로 시작하면 해당 인스턴스도 함수에 의해 닫히기 때문에 모든 Excel 인스턴스를 닫는 옵션은 부적절합니다. 나는 사용자가 행복하지 않을 것입니다 내기! 솔직히 말해서 이것은 절름발이 옵션입니다 (범죄자 없음).해결책 : 메인 창의 hWnd로 Excel 프로세스를 종료 하십시오 (첫 번째 해결책입니다).

간단한 코드는 다음과 같습니다.

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

/// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns>
public static bool TryKillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if(processID == 0) return false;
    try
    {
        Process.GetProcessById((int)processID).Kill();
    }
    catch (ArgumentException)
    {
        return false;
    }
    catch (Win32Exception)
    {
        return false;
    }
    catch (NotSupportedException)
    {
        return false;
    }
    catch (InvalidOperationException)
    {
        return false;
    }
    return true;
}

/// <summary> Finds and kills process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <exception cref="ArgumentException">
/// Thrown when process is not found by the hWnd parameter (the process is not running). 
/// The identifier of the process might be expired.
/// </exception>
/// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception>
public static void KillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if (processID == 0)
        throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd");
    Process.GetProcessById((int)processID).Kill();
}

보시다시피 Try-Parse 패턴에 따라 두 가지 방법을 제공했습니다 (여기서 적절하다고 생각합니다). 프로세스를 종료 할 수없는 경우 한 가지 방법으로 예외가 발생하지 않습니다 (예 : 프로세스가 더 이상 존재하지 않음) 프로세스가 종료되지 않은 경우 다른 메소드에서 예외가 발생합니다. 이 코드에서 유일한 약점은 보안 권한입니다. 이론적으로 사용자는 프로세스를 종료 할 수있는 권한이 없을 수 있지만 모든 경우의 99.99 %에서 그러한 권한이 있습니다. 또한 손님 계정으로 테스트했는데 완벽하게 작동합니다.

따라서 Excel을 사용하는 코드는 다음과 같습니다.

int hWnd = xl.Application.Hwnd;
// ...
// here we try to close Excel as usual, with xl.Quit(),
// Marshal.FinalReleaseComObject(xl) and so on
// ...
TryKillProcessByMainWindowHwnd(hWnd);

짜잔! 엑셀이 종료되었습니다! 🙂

글의 시작 부분에서 약속했듯이 두 번째 해결책으로 돌아가 봅시다.
두 번째 해결책은 GC.Collect () 및 GC.WaitForPendingFinalizers ()를 호출하는 것입니다. 그렇습니다, 그들은 실제로 작동하지만 여기서 조심해야합니다!
많은 사람들이 GC.Collect () 호출이 도움이되지 않는다고 말합니다. 그러나 그것이 도움이되지 않는 이유는 COM 객체에 대한 참조가 여전히 존재하기 때문입니다! GC.Collect ()가 도움이되지 않는 가장 일반적인 이유 중 하나는 프로젝트를 디버그 모드에서 실행하는 것입니다. 더 이상 실제로 참조되지 않는 디버그 모드 개체는 메서드가 끝날 때까지 가비지 수집되지 않습니다.
따라서 GC.Collect () 및 GC.WaitForPendingFinalizers ()를 시도했지만 도움이되지 않으면 다음을 수행하십시오.

1) 릴리스 모드에서 프로젝트를 실행하고 Excel이 올바르게 닫혔는지 확인하십시오.

2) Excel 작업 방법을 별도의 방법으로 래핑하십시오. 따라서 이와 같은 대신 :

void GenerateWorkbook(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

당신은 쓰기:

void GenerateWorkbook(...)
{
  try
  {
    GenerateWorkbookInternal(...);
  }
  finally
  {
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

private void GenerateWorkbookInternal(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
  }
}

이제 Excel이 닫힙니다 =)


답변

업데이트 : C # 코드 추가 및 Windows 작업 링크

나는 언젠가이 문제를 알아 내기 위해 노력했고 XtremeVBTalk가 가장 활발하고 반응이 좋았습니다. 응용 프로그램이 충돌하더라도 내 원래 게시물에 대한 링크는 다음과 같습니다 . Excel Interop 프로세스를 깨끗하게 종료합니다 . 아래는 게시물의 요약과이 게시물에 코드가 복사 된 것입니다.

  • 와 상호 운용성 프로세스를 마감 Application.Quit()하고하는 것은 Process.Kill()대부분의 경우 작동하지만, 응용 프로그램이 격변 충돌하면 실패합니다. 즉, 앱이 충돌하더라도 Excel 프로세스는 여전히 느슨하게 실행됩니다.
  • 해결책은 OS가 Win32 호출을 사용하여 Windows 작업 개체 를 통해 프로세스 정리를 처리하도록하는 것 입니다. 기본 응용 프로그램이 종료되면 관련 프로세스 (예 : Excel)도 종료됩니다.

OS가 실제로 정리 작업을 수행하고 있기 때문에 이것이 깨끗한 솔루션이라는 것을 알았습니다. 당신이해야 할 모든입니다 등록 엑셀 과정을.

Windows 작업 코드

Interop 프로세스를 등록하기 위해 Win32 API 호출을 래핑합니다.

public enum JobObjectInfoType
{
    AssociateCompletionPortInformation = 7,
    BasicLimitInformation = 2,
    BasicUIRestrictions = 4,
    EndOfJobTimeInformation = 6,
    ExtendedLimitInformation = 9,
    SecurityLimitInformation = 5,
    GroupInformation = 11
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public IntPtr lpSecurityDescriptor;
    public int bInheritHandle;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
    public Int64 PerProcessUserTimeLimit;
    public Int64 PerJobUserTimeLimit;
    public Int16 LimitFlags;
    public UInt32 MinimumWorkingSetSize;
    public UInt32 MaximumWorkingSetSize;
    public Int16 ActiveProcessLimit;
    public Int64 Affinity;
    public Int16 PriorityClass;
    public Int16 SchedulingClass;
}

[StructLayout(LayoutKind.Sequential)]
struct IO_COUNTERS
{
    public UInt64 ReadOperationCount;
    public UInt64 WriteOperationCount;
    public UInt64 OtherOperationCount;
    public UInt64 ReadTransferCount;
    public UInt64 WriteTransferCount;
    public UInt64 OtherTransferCount;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
    public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
    public IO_COUNTERS IoInfo;
    public UInt32 ProcessMemoryLimit;
    public UInt32 JobMemoryLimit;
    public UInt32 PeakProcessMemoryUsed;
    public UInt32 PeakJobMemoryUsed;
}

public class Job : IDisposable
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern IntPtr CreateJobObject(object a, string lpName);

    [DllImport("kernel32.dll")]
    static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);

    private IntPtr m_handle;
    private bool m_disposed = false;

    public Job()
    {
        m_handle = CreateJobObject(null, null);

        JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
        info.LimitFlags = 0x2000;

        JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        extendedInfo.BasicLimitInformation = info;

        int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
        IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
        Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);

        if (!SetInformationJobObject(m_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
            throw new Exception(string.Format("Unable to set information.  Error: {0}", Marshal.GetLastWin32Error()));
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion

    private void Dispose(bool disposing)
    {
        if (m_disposed)
            return;

        if (disposing) {}

        Close();
        m_disposed = true;
    }

    public void Close()
    {
        Win32.CloseHandle(m_handle);
        m_handle = IntPtr.Zero;
    }

    public bool AddProcess(IntPtr handle)
    {
        return AssignProcessToJobObject(m_handle, handle);
    }

}

생성자 코드에 대한 참고 사항

  • 생성자에서 info.LimitFlags = 0x2000;가 호출됩니다. 0x2000는 IS JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE열거 값,이 값에 의해 정의된다 MSDN 같이

작업에 대한 마지막 핸들이 닫힐 때 작업과 연관된 모든 프로세스가 종료되도록합니다.

프로세스 ID (PID)를 얻기위한 추가 Win32 API 호출

    [DllImport("user32.dll", SetLastError = true)]
    public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

코드 사용

    Excel.Application app = new Excel.ApplicationClass();
    Job job = new Job();
    uint pid = 0;
    Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid);
    job.AddProcess(Process.GetProcessById((int)pid).Handle);


답변

이것은 내가 작업중 인 프로젝트에서 효과가있었습니다.

excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;

Excel COM 개체에 대한 모든 참조를 완료 할 때 null 로 설정하는 것이 중요하다는 것을 알았습니다 . 여기에는 셀, 시트 및 모든 것이 포함됩니다.


답변

Excel 네임 스페이스에있는 모든 항목을 해제해야합니다. 기간

당신은 할 수 없습니다 :

Worksheet ws = excel.WorkBooks[1].WorkSheets[1];

넌해야 해

Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];

그 다음에 물체를 풀어 놓는다.


답변

먼저- 전화를 걸 거나 Excel interop을 수행 할 필요가 없습니다 . 혼란스러운 안티 패턴이지만 Microsoft를 포함하여 .NET에서 COM 참조를 수동으로 릴리스해야 함을 나타내는 이에 대한 모든 정보가 올바르지 않습니다. 사실 .NET 런타임 및 가비지 수집기는 COM 참조를 정확하게 추적하고 정리합니다. 코드의 경우 상단에서 전체`while (…) 루프를 제거 할 수 있습니다.Marshal.ReleaseComObject(...)Marshal.FinalReleaseComObject(...)

둘째, 프로세스가 종료 될 때 Out-of-process COM 개체에 대한 COM 참조가 정리되도록하려면 (Excel 프로세스가 닫히도록) 가비지 수집기가 실행되도록해야합니다. GC.Collect()및 을 호출하여이 작업을 올바르게 수행 할 수 GC.WaitForPendingFinalizers()있습니다. 이것을 두 번 호출하는 것이 안전하며 사이클도 확실히 정리됩니다 (필요하지는 않지만 이것을 보여주는 예를 감사드립니다).

셋째, 디버거에서 실행될 때 로컬 참조는 메소드가 끝날 때까지 인위적으로 유지되므로 로컬 변수 검사가 작동합니다. 따라서 GC.Collect()호출은 rng.Cells같은 방법으로 객체를 청소하는 데 효과적이지 않습니다 . COM interop을 수행하는 코드를 GC 정리에서 별도의 방법으로 분할해야합니다. (이것은 @nightcoder가 게시 한 답변의 한 부분에서 나에게 중요한 발견이었습니다.)

따라서 일반적인 패턴은 다음과 같습니다.

Sub WrapperThatCleansUp()

    ' NOTE: Don't call Excel objects in here...
    '       Debugger would keep alive until end, preventing GC cleanup

    ' Call a separate function that talks to Excel
    DoTheWork()

    ' Now let the GC clean up (twice, to clean up cycles too)
    GC.Collect()
    GC.WaitForPendingFinalizers()
    GC.Collect()
    GC.WaitForPendingFinalizers()

End Sub

Sub DoTheWork()
    Dim app As New Microsoft.Office.Interop.Excel.Application
    Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
    Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
    app.Visible = True
    For i As Integer = 1 To 10
        worksheet.Cells.Range("A" & i).Value = "Hello"
    Next
    book.Save()
    book.Close()
    app.Quit()

    ' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub

MSDN 및 스택 오버플로 (특히이 질문!)에 대한 많은 게시물을 포함 하여이 문제에 대한 많은 잘못된 정보와 혼란이 있습니다.

마지막으로 좀 더 자세히 살펴보고 올바른 조언을 구할 수 있도록 설득 한 것은 Marshal.ReleaseComObject 블로그 포스트라고 생각 했습니다 .