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


747

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

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

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

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


1
응용 프로그램을 종료하지 않고 Excel.exe를 종료하려고합니까? 귀하의 질문을 완전히 이해하지 못했습니다.
Bryant

3
관리되지 않는 interop 개체를 올바르게 처리하려고합니다. 따라서 사용자가 앱에서 만든 Excel 스프레드 시트를 완료 한 경우에도 Excel 프로세스가 중단되지 않습니다.
HAdes

3
XML Excel 파일을 생성하여 시도 할 수 없다면 VSTO 비 관리 형 메모리 관리를 고려하십시오. jake.ginnivan.net/vsto-com-interop
Jeremy Thompson

이것이 Excel로 잘 번역됩니까?
쿠프

답변:


684

응용 프로그램이 여전히 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와 개체


16
그런 다음 COM에서 Excel을 사용하지 말고 모든 문제를 해결하십시오. Excel 2007 형식은 Excel을 열지 않고도 사용할 수 있습니다.
user7116

5
나는 "두 개의 점"이 무엇을 의미하는지 이해하지 못했습니다. 설명해 주시겠습니까?
A9S6

22
즉, comObject.Property.PropertiesProperty 패턴을 사용해서는 안됩니다 (두 개의 점이 보입니까?). 대신 comObject.Property를 변수에 할당하고 해당 변수를 사용 및 폐기하십시오. 위 규칙의보다 공식적인 버전은 sth 일 수 있습니다. "com 객체를 사용하기 전에 변수에 할당하십시오. 여기에는 다른 com 객체의 속성 인 com 객체가 포함됩니다."
VVS

5
@Nick : 실제로 가비지 수집기에서 자동으로 처리하므로 정리 작업이 필요 없습니다. GC가 알 수 있도록 모든 COM 객체를 자체 변수에 할당하는 것이 유일한 방법입니다.
VVS

12
@VSS는 볼록이므로 래퍼 변수가 .net 프레임 워크로 만들어지기 때문에 GC가 모든 것을 정리합니다. GC를 청소하는 데 시간이 오래 걸릴 수 있습니다. 많은 interop 후 GC.Collect를 호출하는 것은 나쁜 생각가가 아닙니다.
CodingBarfield

280

실제로 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 #을 사용할 때와 정확히 동일합니다.


3
이와 관련된 논의는 여기에 ExtremeVBTalk .NET 사무 자동화 포럼에서 찾을 수 있습니다 : xtremevbtalk.com/showthread.php?t=303928를 .
Mike Rosenblum

2
다른 모든 실패하면, 그때 Process.Kill () (최후의 수단으로) 사용할 수 있습니다 여기에 설명 : stackoverflow.com/questions/51462/...
마이크 로젠 블럼

1
그것이 리차드 일을 다행입니다. :-) 그리고 여기에 "두 개의 점"을 피하는 것만으로는 문제를 예방하기에 충분하지 않은 미묘한 예가 있습니다 : stackoverflow.com/questions/4964663/…
Mike Rosenblum

이에 대한 의견은 Microsoft의 Misha Shneerson이 2010 년 10 월 7 일의 댓글 날짜 내에 해당 주제에 대한 의견을 제시 한 것입니다. ( blogs.msdn.com/b/csharpfaq/archive/2010/09/28/… )
Mike Rosenblum

이것이 우리의 지속적인 문제에 도움이되기를 바랍니다. finally 블록 내에서 가비지 수집 및 해제 호출을 수행하는 것이 안전합니까?
Brett Green

213

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

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이 닫힙니다 =)


19
스레드가 이미 너무 오래되어서 당신의 훌륭한 답변이 훨씬 아래에 나타나서 슬퍼하는 이유는 더 이상 시간을 피하지 않는 유일한 이유라고 생각합니다.
chiccodoro

15
나는 당신의 대답을 처음 읽었을 때, 그것이 거대한 멍청이라고 생각했습니다. 이것으로 약 6 시간 동안 레슬링을 한 후에 (모든 것이 릴리스되고, 이중 점 등이 없습니다.), 나는 당신의 대답이 천재라고 생각합니다.
Mark

3
고마워 내가 이것을 발견하기 전에 며칠 동안 닫히지 않을 Excel과 씨름했습니다. 우수한.
BBlake

2
@ nightcoder : 정말 자세한 답변입니다. 디버그 모드에 대한 귀하의 의견은 매우 사실이며 이것을 지적한 것이 중요합니다. 릴리스 모드의 동일한 코드는 종종 괜찮을 수 있습니다. 그러나 Process.Kill은 자동화를 사용하는 경우에만 유용합니다. 예를 들어 관리되는 COM 추가 기능과 같은 프로그램이 Excel에서 실행될 때는 아닙니다.
Mike Rosenblum

2
@ DanM : 귀하의 접근 방식은 100 % 정확하며 절대 실패하지 않습니다. 모든 변수를 메서드에 로컬로 유지하면 .NET 코드를 통해 모든 참조에 효과적으로 도달 할 수 없습니다. 즉, 마지막 섹션에서 GC.Collect ()를 호출하면 모든 COM 개체의 종료자가 확실하게 호출됩니다. 그런 다음 각 종료자는 완료되는 개체에서 Marshal.FinalReleaseComObject를 호출합니다. 따라서 귀하의 접근 방식은 간단하고 바보입니다. 이것을 사용하는 것을 두려워하지 마십시오. (주의 사항 : VSTO를 사용하는 경우 의심 스럽지만 GC.Collect () & GC.WaitForPendingFinalizers TWICE에 전화해야합니다.)
Mike Rosenblum

49

업데이트 : 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);

2
프로세스 외부 COM 서버 (다른 COM 클라이언트에 서비스를 제공 할 수 있음)를 명시 적으로 종료하는 것은 두려운 생각입니다. 당신이 이것에 의지한다면 그것은 COM 프로토콜이 고장 났기 때문입니다. COM 서버는 일반적인 윈도우 앱으로 취급되어서는 안됩니다
MickyD

38

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

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

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


30

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

당신은 할 수 없습니다 :

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

넌해야 해

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

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


3
어떻게 aobut xlRange.Interior.Color 예를 들어.
HAdes

인테리어는 (네임 스페이스에서) 릴리즈되어야합니다 ... 반면에 컬러는 그렇지 않습니다 (System.Drawing.Color, iirc에서 발생)
MagicKat

2
실제로 색상은 .Net 색상이 아닌 Excel 색상입니다. 당신은 단지 Long을 전달합니다. 또한 통합 문서 def. 워크 시트를 공개해야합니다.
익명 유형

그것은 사실이 아닙니다. 두 점 나쁜, 한 점 좋은 규칙은 말도 안됩니다. 또한 통합 문서, 워크 시트, 범위, 즉 GC 작업을 해제하지 않아도됩니다. 잊지 말아야 할 유일한 것은 유일한 Excel.Application 개체에서 Quit을 호출하는 것입니다. Quit를 호출 한 후 Excel.Application 객체를 null로 설정하고 GC.Collect ()를 호출합니다. GC.WaitForPendingFinalizers (); 두번.
Dietrich Baumgarten

29

먼저- 전화를 걸 거나 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 블로그 포스트라고 생각 했습니다 .


5
이 페이지의 거의 모든 답변이이 페이지를 제외하고 잘못되었습니다. 그들은 작동하지만 너무 많은 일입니다. "2 DOTS"규칙을 무시하십시오. GC가 당신을 위해 일하도록하십시오. 닷넷 GURU 한스 옆모습에서 보충 증거 : stackoverflow.com/a/25135685/3852958
앨런 Baljeu

1
Govert, 올바른 방법을 찾는 것이 얼마나 안도감입니까. 감사합니다.
Dietrich Baumgarten

18

내가 발견 캔 도움이 COM 개체에 대한 올바른 처리 패턴, Marshal.ReleaseComObject 그들이 범위의 외출 때 호출 그 필요를 구현하는 것이 유용한 일반적인 템플릿 :

용법:

using (AutoReleaseComObject<Application> excelApplicationWrapper = new AutoReleaseComObject<Application>(new Application()))
{
    try
    {
        using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapper.ComObject.Workbooks.Open(namedRangeBase.FullName, false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing)))
        {
           // do something with your workbook....
        }
    }
    finally
    {
         excelApplicationWrapper.ComObject.Quit();
    } 
}

주형:

public class AutoReleaseComObject<T> : IDisposable
{
    private T m_comObject;
    private bool m_armed = true;
    private bool m_disposed = false;

    public AutoReleaseComObject(T comObject)
    {
        Debug.Assert(comObject != null);
        m_comObject = comObject;
    }

#if DEBUG
    ~AutoReleaseComObject()
    {
        // We should have been disposed using Dispose().
        Debug.WriteLine("Finalize being called, should have been disposed");

        if (this.ComObject != null)
        {
            Debug.WriteLine(string.Format("ComObject was not null:{0}, name:{1}.", this.ComObject, this.ComObjectName));
        }

        //Debug.Assert(false);
    }
#endif

    public T ComObject
    {
        get
        {
            Debug.Assert(!m_disposed);
            return m_comObject;
        }
    }

    private string ComObjectName
    {
        get
        {
            if(this.ComObject is Microsoft.Office.Interop.Excel.Workbook)
            {
                return ((Microsoft.Office.Interop.Excel.Workbook)this.ComObject).Name;
            }

            return null;
        }
    }

    public void Disarm()
    {
        Debug.Assert(!m_disposed);
        m_armed = false;
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

    #endregion

    protected virtual void Dispose(bool disposing)
    {
        if (!m_disposed)
        {
            if (m_armed)
            {
                int refcnt = 0;
                do
                {
                    refcnt = System.Runtime.InteropServices.Marshal.ReleaseComObject(m_comObject);
                } while (refcnt > 0);

                m_comObject = default(T);
            }

            m_disposed = true;
        }
    }
}

참고:

http://www.deez.info/sengelha/2005/02/11/useful-idisposable-class-3-autoreleasecomobject/


3
응 이거 좋다 FinalReleaseCOMObject를 사용할 수 있지만이 코드를 업데이트 할 수 있다고 생각합니다.
익명 유형

각 객체에 대해 using 블록을 사용하는 것은 여전히 ​​성가신 일입니다. 대안 은 여기 stackoverflow.com/questions/2191489/… 를 참조하십시오 .
Henrik

16

이 문제가 5 년 동안 세상을 괴롭혔다는 것을 믿을 수 없습니다 .... 응용 프로그램을 만든 경우 먼저 링크를 제거하기 전에 종료해야합니다.

objExcel = new Excel.Application();  
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing)); 

닫을 때

objBook.Close(true, Type.Missing, Type.Missing); 
objExcel.Application.Quit();
objExcel.Quit(); 

엑셀 응용 프로그램을 새로 만들면 백그라운드에서 엑셀 프로그램이 열립니다. 링크를 해제하기 전에 해당 엑셀 프로그램이 직접 제어의 일부가 아니므로 엑셀 프로그램을 종료하도록 명령해야합니다. 따라서 링크가 해제되면 계속 열려 있습니다!

좋은 프로그래밍 여러분 ~~


아니요, 특히 COM 응용 프로그램과의 사용자 상호 작용을 허용하려는 경우에는 그렇지 않습니다 (이 경우 Excel – 사용자 상호 작용의 의도 여부를 지정하지 않지만 종료에 대한 참조는 없음). 사용자가 단일 파일을 닫을 때 Excel을 종료 할 수 있도록하려면 호출자를 가져와야합니다.
downwitch

이것은 나를 위해 완벽하게 작동했습니다-objExcel.Quit () 만 있었을 때 충돌이 발생했지만 objExcel.Application.Quit ()도 사전에 깨끗하게 닫혔습니다.
mcmillab

13

일반적인 개발자는 귀하의 솔루션 중 어느 것도 나를 위해 일하지 않았으므로 새로운 트릭 을 구현하기로 결정했습니다 .

먼저 "우리의 목표는 무엇입니까?"를 지정하십시오. => "작업 관리자에서 작업 후 엑셀 객체를 볼 수 없음"

확인. 도전하고 파괴를 시작하지 말고 병렬로 실행중인 다른 인스턴스 os Excel을 파괴하지 마십시오.

따라서 현재 프로세서 목록을 가져 와서 EXCEL 프로세스의 PID를 가져온 다음 작업이 완료되면 프로세스 목록에 고유 한 PID를 가진 새 게스트가 있고 그 중 하나만 찾아 파괴합니다.

<엑셀 작업 중 새로운 엑셀 프로세스는 새롭고 파괴 된 것으로 감지됩니다.

Process[] prs = Process.GetProcesses();
List<int> excelPID = new List<int>();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL")
       excelPID.Add(p.Id);

.... // your job 

prs = Process.GetProcesses();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL" && !excelPID.Contains(p.Id))
       p.Kill();

이것은 내 문제를 해결하고 당신도 희망하십시오.


1
.... 이것은 약간의 썰매 방식입니다? -그것은 또한 내가 사용하고있는 것과 비슷합니다 : (그러나 변경해야합니다.이 접근법을 사용하는 문제는 종종이 기계가 달린 기계에서 엑셀을 열 때 왼쪽 막대에 경고가 있다는 것입니다. "엑셀이 잘못 닫혔습니다":별로 우아하지 않습니다.이 글의 앞선 제안 중 하나가 선호
된다고 생각합니다

11

이것은 너무 복잡해 보입니다. 내 경험상 Excel을 올바르게 닫을 수있는 세 가지 주요 사항이 있습니다.

1 : 메이크업 확실히 더 만든 엑셀 응용 프로그램에 대한 참조가 남아있다 (만 어쨌든 일이 없다한다 세트가에를 null)

2 : 전화 GC.Collect()

3 : 프로그램을 수동으로 닫거나 QuitExcel 개체 를 호출 하여 Excel을 닫아야 합니다. ( Quit이것은 사용자가 프로그램을 닫으려고 한 것처럼 작동하며 Excel이 보이지 않아도 변경 사항이 저장되지 않은 경우 확인 대화 상자를 표시합니다. 사용자가 취소를 누르면 Excel이 닫히지 않습니다. )

1은 2 이전에 발생해야하지만 3은 언제든지 발생할 수 있습니다.

이것을 구현하는 한 가지 방법은 interop Excel 객체를 자신의 클래스로 감싸고 생성자에 interop 인스턴스를 만들고 Dispose와 같은 IDisposable을 구현하는 것입니다.

if (!mDisposed) {
   mExcel = null;
   GC.Collect();
   mDisposed = true;
}

그것은 프로그램의 측면에서 뛰어납니다. Excel이 닫히면 (수동으로 또는 사용자가 호출 Quit) 프로세스가 사라집니다. 프로그램이 이미 종료 된 경우 프로세스는 GC.Collect()호출시 사라집니다 .

(얼마나 중요한지 잘 모르겠지만 GC.WaitForPendingFinalizers()통화 후 전화를 원할 수도 GC.Collect()있지만 Excel 프로세스를 제거하는 데 반드시 필요한 것은 아닙니다.)

이것은 몇 년 동안 문제없이 저에게 효과적이었습니다. 이것이 작동하는 동안 실제로 작동하려면 정상적으로 종료해야합니다. Excel을 정리하기 전에 프로그램을 중단 한 경우 (일반적으로 프로그램을 디버깅하는 동안 "중지"를 누름) 여전히 excel.exe 프로세스가 누적됩니다.


1
이 답변은 받아 들여지는 답변보다 훨씬 편리합니다. COM 객체를 사용한 "두 개의 점"에 대해 걱정할 필요가 없습니다 Marshal.
andrew.cuthbert

9

읽기, 생성시 각 개체에 대한 직접 굴절을 생성하더라도 Excel이 닫히지 않는 이유를 추가하려면 'For'루프가 필요합니다.

For Each objWorkBook As WorkBook in objWorkBooks 'local ref, created from ExcelApp.WorkBooks to avoid the double-dot
   objWorkBook.Close 'or whatever
   FinalReleaseComObject(objWorkBook)
   objWorkBook = Nothing
Next 

'The above does not work, and this is the workaround:

For intCounter As Integer = 1 To mobjExcel_WorkBooks.Count
   Dim objTempWorkBook As Workbook = mobjExcel_WorkBooks.Item(intCounter)
   objTempWorkBook.Saved = True
   objTempWorkBook.Close(False, Type.Missing, Type.Missing)
   FinalReleaseComObject(objTempWorkBook)
   objTempWorkBook = Nothing
Next

또 다른 이유는 이전 값을 먼저 공개하지 않고 참조를 재사용하는 것입니다.
Grimfort

감사합니다. 나는 또한 당신의 솔루션이 나를 위해 일한 "각각"을 사용하고있었습니다.
차드 브라운-두인

+1 감사합니다. 또한 Excel 범위의 객체가 있고 객체 수명 기간 동안 범위를 변경하려는 경우 객체를 다시 할당하기 전에 ReleaseComObject를 사용해야한다는 것을 알았습니다.
AjV Jsy

9

나는 전통적으로 VVS의 답변 에서 찾은 조언을 따랐습니다 . 그러나 최신 옵션을 사용하여이 답변을 최신 상태로 유지하기 위해 앞으로의 모든 프로젝트에서 "NetOffice"라이브러리를 사용한다고 생각합니다.

NetOffice 는 Office PIA를 완전히 대체하며 버전에 구애받지 않습니다. .NET에서 Microsoft Office와 함께 작업 할 때 종종 두통을 일으키는 정리를 처리 할 수있는 관리되는 COM 래퍼 모음입니다.

주요 기능은 다음과 같습니다.

  • 대부분 버전에 독립적이며 버전에 따른 기능이 문서화되어 있습니다.
  • 의존성 없음
  • PIA 없음
  • 등록 안함
  • VSTO 없음

나는 결코 프로젝트와 관련이 없습니다. 두통이 크게 줄어든 것에 대해 진심으로 감사드립니다.


2
이것은 실제로 답변으로 표시되어야합니다. NetOffice는이 모든 복잡성을 추상화합니다.
C. Augusto Proiete

1
Excel Add-in을 작성하는 데 상당한 시간 동안 NetOffice를 사용해 왔으며 완벽하게 작동했습니다. 고려해야 할 유일한 점은 사용 된 개체를 명시 적으로 폐기하지 않으면 응용 프로그램을 종료 할 때 작업을 수행한다는 것입니다 (어쨌든 추적하기 때문에). 따라서 NetOffice의 경험상 규칙은 항상 셀, 범위 또는 시트와 같은 모든 Excel 개체에 "사용"패턴을 사용하는 것입니다.
Stas Ivanov

8

여기에 허용 된 대답은 정확하지만 "두 개의 점"참조는 피해야 할뿐만 아니라 인덱스를 통해 검색되는 개체도 피해야합니다. 또한 이러한 객체를 정리하기 위해 프로그램을 완료 할 때까지 기다릴 필요가 없습니다. 가능하면 완료 한 즉시 정리할 함수를 작성하는 것이 가장 좋습니다. 다음은 Style 객체의 일부 속성을 할당하는 함수입니다 xlStyleHeader.

public Excel.Style xlStyleHeader = null;

private void CreateHeaderStyle()
{
    Excel.Styles xlStyles = null;
    Excel.Font xlFont = null;
    Excel.Interior xlInterior = null;
    Excel.Borders xlBorders = null;
    Excel.Border xlBorderBottom = null;

    try
    {
        xlStyles = xlWorkbook.Styles;
        xlStyleHeader = xlStyles.Add("Header", Type.Missing);

        // Text Format
        xlStyleHeader.NumberFormat = "@";

        // Bold
        xlFont = xlStyleHeader.Font;
        xlFont.Bold = true;

        // Light Gray Cell Color
        xlInterior = xlStyleHeader.Interior;
        xlInterior.Color = 12632256;

        // Medium Bottom border
        xlBorders = xlStyleHeader.Borders;
        xlBorderBottom = xlBorders[Excel.XlBordersIndex.xlEdgeBottom];
        xlBorderBottom.Weight = Excel.XlBorderWeight.xlMedium;
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        Release(xlBorderBottom);
        Release(xlBorders);
        Release(xlInterior);
        Release(xlFont);
        Release(xlStyles);
    }
}

private void Release(object obj)
{
    // Errors are ignored per Microsoft's suggestion for this type of function:
    // http://support.microsoft.com/default.aspx/kb/317109
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
    }
    catch { } 
}

내가 설정해야한다는 것을 주목하십시오. xlBorders[Excel.XlBordersIndex.xlEdgeBottom]정리하기 위해 변수 해야합니다 (두 개의 점이 아니라 해제 할 필요가없는 열거를 참조하는 것이 아니라 내가 참조하는 객체가 실제로 Border 객체이기 때문에) 해제해야합니다).

이러한 종류의 작업은 표준 응용 프로그램에서 실제로 필요하지는 않지만 자체적으로 정리 작업을 많이 수행하는 ASP.NET 응용 프로그램에서는 가비지 수집기를 호출하는 빈도에 관계없이 ASP.NET 응용 프로그램 중 하나라도 놓치면 Excel에서 여전히 서버에서 실행 중입니다.

이 코드를 작성할 때 작업 관리자를 모니터링하는 동안 세부 사항과 많은 테스트 실행에 많은주의를 기울여야하지만 이렇게하면 누락 된 한 인스턴스를 찾기 위해 코드 페이지를 필사적으로 검색해야하는 번거 로움이 줄어 듭니다. 루프 할 때마다 동일한 변수 이름을 사용하더라도 오브젝트의 각 인스턴스를 해제해야하는 루프에서 작업 할 때 특히 중요합니다.


내부 릴리스에서 Null (Joe 's answer)을 확인하십시오. 불필요한 null 예외를 피할 수 있습니다. 많은 방법을 테스트했습니다. 이것이 Excel을 효과적으로 해제하는 유일한 방법입니다.
게르하르트 파월

8

시도 후

  1. COM 개체를 역순으로 해제
  2. 추가 GC.Collect()하고GC.WaitForPendingFinalizers() 마지막에 두 번
  3. 두 개의 점 이상
  4. 통합 문서를 닫고 응용 프로그램을 종료
  5. 릴리스 모드에서 실행

나를 위해 일하는 마지막 해결책은 한 세트를 옮기는 것입니다.

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

함수의 끝에 다음과 같이 랩퍼에 추가했습니다.

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

ComObjects를 무효화 할 필요가 없으며 Quit ()-Close () 및 FinalReleaseComObject 만 필요합니다. 나는 이것이 작동하기 위해 내 말에 빠진 모든 것을 믿을 수 없다. 큰!
Carol

7

나는 이것을 정확하게 따랐다. 그러나 여전히 1000 번 중 1 번 문제가 발생했다. 왜 그런지 누가 알 겠어요? 망치를 꺼내는 시간 ...

Excel Application 클래스가 인스턴스화 된 직후에 방금 생성 된 Excel 프로세스가 보류됩니다.

excel = new Microsoft.Office.Interop.Excel.Application();
var process = Process.GetProcessesByName("EXCEL").OrderByDescending(p => p.StartTime).First();

그런 다음 위의 COM 정리를 모두 마치면 프로세스가 실행되고 있지 않은지 확인합니다. 여전히 실행 중이면 종료하십시오!

if (!process.HasExited)
   process.Kill();

7

¨ ° º¤ø „¸ 엑셀 가공 및 풍선 껌 씹기 ¸ „ø¤º ° ¨

public class MyExcelInteropClass
{
    Excel.Application xlApp;
    Excel.Workbook xlBook;

    public void dothingswithExcel() 
    {
        try { /* Do stuff manipulating cells sheets and workbooks ... */ }
        catch {}
        finally {KillExcelProcess(xlApp);}
    }

    static void KillExcelProcess(Excel.Application xlApp)
    {
        if (xlApp != null)
        {
            int excelProcessId = 0;
            GetWindowThreadProcessId(xlApp.Hwnd, out excelProcessId);
            Process p = Process.GetProcessById(excelProcessId);
            p.Kill();
            xlApp = null;
        }
    }

    [DllImport("user32.dll")]
    static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
}

6

Excel은 현재 실행중인 문화에 매우 민감하다는 것을 알고 있어야합니다.

Excel 함수를 호출하기 전에 문화권을 EN-US로 설정해야 할 수도 있습니다. 이것은 모든 기능에 적용되는 것이 아니라 일부 기능에 적용됩니다.

    CultureInfo en_US = new System.Globalization.CultureInfo("en-US"); 
    System.Threading.Thread.CurrentThread.CurrentCulture = en_US;
    string filePathLocal = _applicationObject.ActiveWorkbook.Path;
    System.Threading.Thread.CurrentThread.CurrentCulture = orgCulture;

VSTO를 사용하는 경우에도 적용됩니다.

자세한 내용 : http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369


6

"COM 개체에 두 개의 점을 사용하지 마십시오"는 COM 참조의 누수를 피하기위한 가장 좋은 방법이지만 Excel PIA는 첫눈에 보이는 것보다 더 많은 방식으로 누수가 발생할 수 있습니다.

이러한 방법 중 하나는 Excel 개체 모델의 COM 개체에 의해 노출되는 모든 이벤트를 구독하는 것입니다.

예를 들어 Application 클래스의 WorkbookOpen 이벤트를 구독합니다.

COM 이벤트에 관한 이론

COM 클래스는 콜백 인터페이스를 통해 이벤트 그룹을 노출합니다. 이벤트를 구독하기 위해 클라이언트 코드는 콜백 인터페이스를 구현하는 개체를 간단히 등록 할 수 있으며 COM 클래스는 특정 이벤트에 대한 응답으로 메서드를 호출합니다. 콜백 인터페이스는 COM 인터페이스이기 때문에 구현 객체는 이벤트 핸들러에 대해 수신 한 모든 COM 객체 (매개 변수)의 참조 카운트를 감소시켜야합니다.

Excel PIA가 COM 이벤트를 노출하는 방법

Excel PIA는 Excel Application 클래스의 COM 이벤트를 일반적인 .NET 이벤트로 노출합니다. 클라이언트 코드 가 .NET 이벤트를 구독 할 때마다 ( 'a'에 강조 표시) PIA 콜백 인터페이스를 구현하는 클래스 인스턴스를 Excel에 등록합니다.

따라서 많은 콜백 개체가 .NET 코드의 다른 구독 요청에 대한 응답으로 Excel에 등록됩니다. 이벤트 구독 당 하나의 콜백 오브젝트.

이벤트 처리를위한 콜백 인터페이스는 PIA가 모든 .NET 이벤트 구독 요청에 대한 모든 인터페이스 이벤트를 구독해야 함을 의미합니다. 선택할 수 없습니다. 이벤트 콜백을 수신하면 콜백 객체는 연결된 .NET 이벤트 핸들러가 현재 이벤트에 관심이 있는지 확인한 다음 핸들러를 호출하거나 콜백을 자동으로 무시합니다.

COM 인스턴스 참조 수에 미치는 영향

이러한 모든 콜백 객체는 콜백 메소드 (자동으로 무시되는 메소드의 경우에도)에 대해 수신 한 COM 객체 (매개 변수)의 참조 횟수를 감소시키지 않습니다. COM 개체를 비우기 위해 CLR 가비지 수집기 에만 의존 합니다.

GC 실행은 결정적이지 않기 때문에 원하는 것보다 더 오랜 시간 동안 Excel 프로세스를 보류하고 '메모리 누수'라는 인상을 줄 수 있습니다.

해결책

현재 유일한 해결책은 COM 클래스에 대한 PIA의 이벤트 공급자를 피하고 COM 개체를 결정적으로 해제하는 고유 한 이벤트 공급자를 작성하는 것입니다.

Application 클래스의 경우 AppEvents 인터페이스를 구현 한 다음 IConnectionPointContainer interface 를 사용하여 Excel에 구현을 등록하면 됩니다 . 응용 프로그램 클래스 (및 콜백 메커니즘을 사용하여 이벤트를 노출하는 모든 COM 개체)는 IConnectionPointContainer 인터페이스를 구현합니다.


4

다른 사람들이 지적했듯이, 이 KB 기사에 설명 된대로 사용하는 모든 Excel 개체에 대한 명시 적 참조를 만들고 해당 참조에 대해 Marshal.ReleaseComObject를 호출해야합니다 . 또한 예외가 발생하더라도 ReleaseComObject가 항상 호출되도록 try / finally를 사용해야합니다. 즉, 대신 :

Worksheet sheet = excelApp.Worksheets(1)
... do something with sheet

다음과 같은 작업을 수행해야합니다.

Worksheets sheets = null;
Worksheet sheet = null
try
{ 
    sheets = excelApp.Worksheets;
    sheet = sheets(1);
    ...
}
finally
{
    if (sheets != null) Marshal.ReleaseComObject(sheets);
    if (sheet != null) Marshal.ReleaseComObject(sheet);
}

Excel을 닫으려면 Application 개체를 해제하기 전에 Application.Quit을 호출해야합니다.

보시다시피, 약간 복잡한 작업을 시도하자마자 이것은 매우 다루기 힘들어집니다. Excel 개체 모델의 몇 가지 간단한 조작 (워크 북 열기, 범위에 쓰기, 통합 문서 저장 / 닫기 등)을 래핑하는 간단한 래퍼 클래스를 사용하여 .NET 응용 프로그램을 성공적으로 개발했습니다. 래퍼 클래스는 IDisposable을 구현하고 사용하는 모든 개체에 Marshal.ReleaseComObject를 신중하게 구현하며 Excel 개체를 나머지 앱에 공개적으로 노출시키지 않습니다.

그러나이 방법은보다 복잡한 요구 사항에 적합하지 않습니다.

이것은 .NET COM Interop의 큰 결함입니다. 더 복잡한 시나리오의 경우 Office와 같은 out-proc COM 개체와의 모든 상호 작용을 위임 할 수있는 VB6 또는 기타 관리되지 않는 언어로 ActiveX DLL을 작성하는 것이 중요합니다. 그런 다음 .NET 응용 프로그램에서이 ActiveX DLL을 참조 할 수 있으며이 참조 하나만 해제하면되므로 훨씬 쉽습니다.


3

위의 모든 항목이 작동하지 않으면 Excel에서 시트를 닫을 시간을주십시오.

app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();

...
FinalReleaseComObject(app);

2
나는 임의의 대기가 싫어. 그러나 통합 문서가 닫힐 때까지 기다릴 필요가 있기 때문에 +1입니다. 대안은 루프에서 통합 문서 컬렉션을 폴링하고 루프 대기 시간으로 임의 대기를 사용하는 것입니다.
dFlat

3

Excel과 관련된 모든 개체를 해제해야합니다!

몇 가지 방법으로 몇 시간을 보냈습니다. 모두 훌륭한 아이디어이지만 마침내 내 실수를 발견했습니다. 모든 물건을 풀지 않으면 위의 방법 으로 내 경우처럼 도움이 될 수 없습니다 . 범위 1을 포함한 모든 물체를 놓아야합니다!

Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);

옵션은 여기에 있습니다 .


아주 좋은 지적입니다! Office 2007을 사용하고 있기 때문에 본인을 위해 정리 작업이 완료된 것 같습니다. 나는 권고를 더 많이 사용했지만 여기에 제안한대로 변수를 저장하지 않았으며 EXCEL.EXE는 종료되지만 운이 좋을 수 있으며 더 이상 문제가있는 경우 분명히 내 코드 의이 부분을 볼 것입니다 =)
Coops

3

COM 개체 릴리스 에 대한 유용한 기사는 2.5 MSDN ( COM 개체 해제 )입니다.

내가 옹호 것이이 방법은 비 지역 변수가있는 경우 Excel.Interop 참조를 null로하고 호출하는 것입니다 GC.Collect()GC.WaitForPendingFinalizers()두 번. 로컬 범위의 Interop 변수는 자동으로 처리됩니다.

따라서 모든 COM 개체에 대해 명명 된 참조를 유지할 필요가 없습니다 .

다음은 기사에서 가져온 예입니다.

public class Test {

    // These instance variables must be nulled or Excel will not quit
    private Excel.Application xl;
    private Excel.Workbook book;

    public void DoSomething()
    {
        xl = new Excel.Application();
        xl.Visible = true;
        book = xl.Workbooks.Add(Type.Missing);

        // These variables are locally scoped, so we need not worry about them.
        // Notice I don't care about using two dots.
        Excel.Range rng = book.Worksheets[1].UsedRange;
    }

    public void CleanUp()
    {
        book = null;
        xl.Quit();
        xl = null;

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

이 단어는 기사에서 직접 나온 것입니다.

거의 모든 상황에서 RCW 참조를 무효화하고 가비지 콜렉션을 강제 실행하면 올바르게 정리됩니다. 또한 GC.WaitForPendingFinalizers를 호출하면 가비지 콜렉션이 가능한 한 결정적입니다. 즉, 두 번째 호출에서 WaitForPendingFinalizers로 돌아올 때 개체가 정리 된 시점을 정확하게 확인할 수 있습니다. 대안으로 Marshal.ReleaseComObject를 사용할 수 있습니다. 그러나이 방법을 사용할 필요는 거의 없습니다.


2

두 점 규칙이 저에게 효과가 없었습니다. 필자의 경우 다음과 같이 리소스를 청소하는 방법을 만들었습니다.

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}

2

내 솔루션

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

private void GenerateExcel()
{
    var excel = new Microsoft.Office.Interop.Excel.Application();
    int id;
    // Find the Excel Process Id (ath the end, you kill him
    GetWindowThreadProcessId(excel.Hwnd, out id);
    Process excelProcess = Process.GetProcessById(id);

try
{
    // Your code
}
finally
{
    excel.Quit();

    // Kill him !
    excelProcess.Kill();
}

2

Word / Excel interop 응용 프로그램을 사용할 때는 매우주의해야합니다. 모든 솔루션을 시도한 후에도 여전히 많은 "WinWord"프로세스가 서버에 남아있었습니다 (사용자가 2000 명 이상임).

몇 시간 동안 문제를 해결 한 후 다음을 사용하여 두 개 이상의 문서를 열면 Word.ApplicationClass.Document.Open() 다른 스레드에서 동시에 IIS 작업자 프로세스 (w3wp.exe)가 충돌하여 모든 WinWord 프로세스가 열려 있습니다!

따라서이 문제에 대한 절대적인 해결책은 없지만 Office Open XML 개발 과 같은 다른 방법으로 전환한다고 생각 합니다.


1

받아 들인 대답이 효과가 없었습니다. 소멸자의 다음 코드가 작업을 수행했습니다.

if (xlApp != null)
{
    xlApp.Workbooks.Close();
    xlApp.Quit();
}

System.Diagnostics.Process[] processArray = System.Diagnostics.Process.GetProcessesByName("EXCEL");
foreach (System.Diagnostics.Process process in processArray)
{
    if (process.MainWindowTitle.Length == 0) { process.Kill(); }
}


1

나는 현재 사무 자동화를 위해 노력하고 있으며 나를 위해 매번 작동하는 솔루션을 우연히 발견했습니다. 간단하며 프로세스 종료와 관련이 없습니다.

단순히 현재 활성 프로세스를 반복하고 열려있는 Excel 프로세스에 '액세스'하는 방식으로 모든 부유 매달려있는 Excel 인스턴스가 제거되는 것으로 보입니다. 아래 코드는 단순히 이름이 'Excel'인 프로세스를 확인한 다음 프로세스의 MainWindowTitle 속성을 문자열에 씁니다. 이 프로세스와의 '상호 작용'은 Windows가 고정 된 Excel 인스턴스를 따라 잡고 중단시키는 것처럼 보입니다.

언로드 이벤트를 시작하기 때문에 개발중인 추가 기능 바로 전에 아래 방법을 실행합니다. 매번 Excel의 매달린 인스턴스를 제거합니다. 모든 정직에서 이것이 왜 작동하는지 완전히 확신하지는 못하지만 그것이 잘 작동하며 더블 도트, Marshal.ReleaseComObject 또는 프로세스 종료에 대해 걱정할 필요없이 Excel 응용 프로그램의 끝에 배치 할 수 있습니다. 이것이 왜 효과적인 지에 대한 제안에 매우 관심이 있습니다.

public static void SweepExcelProcesses()
{           
            if (Process.GetProcessesByName("EXCEL").Length != 0)
            {
                Process[] processes = Process.GetProcesses();
                foreach (Process process in processes)
                {
                    if (process.ProcessName.ToString() == "excel")
                    {                           
                        string title = process.MainWindowTitle;
                    }
                }
            }
}

1

그 중 일부는 프레임 워크가 Office 응용 프로그램을 처리하는 방식 일 뿐이라고 생각하지만 잘못 될 수 있습니다. 어떤 날에는 일부 응용 프로그램에서 프로세스를 즉시 정리하고 다른 날에는 응용 프로그램이 닫힐 때까지 기다리는 것 같습니다. 일반적으로 세부 사항에주의를 기울이고 하루가 끝날 때 추가 프로세스가 없는지 확인합니다.

또한 어쩌면 나는 일을 단순화하는 것이 끝났지 만 당신이 할 수 있다고 생각합니다 ...

objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
DoSomeStuff(objBook);
SaveTheBook(objBook);
objBook.Close(false, Type.Missing, Type.Missing);
objExcel.Quit();

앞서 말했듯이 Excel 프로세스가 나타나거나 사라지는시기에 대한 세부 사항에주의를 기울이지 않지만 일반적으로 저에게 효과적입니다. 또한 Excel 프로세스를 최소한의 시간 이외의 시간 동안 유지하는 것을 좋아하지 않지만 아마도 편집증 일 것입니다.


1

일부는 이미 작성 했으므로 Excel (객체) 을 닫는 방법은 중요하지 않습니다 . 또한 파일을 여는 방법 과 프로젝트 유형에 따라 중요합니다 .

WPF 응용 프로그램에서 기본적으로 동일한 코드는 거의 문제없이 작동합니다.

동일한 매개 변수 값에 대해 동일한 Excel 파일을 여러 번 처리하는 프로젝트가 있습니다 (예 : 일반 목록 내의 값을 기준으로 파싱).

모든 Excel 관련 함수를 기본 클래스에 넣고 파서를 하위 클래스에 넣습니다 (다른 파서는 일반적인 Excel 함수를 사용합니다). 일반 목록의 각 항목에 대해 Excel을 열고 닫는 것을 원하지 않았으므로 기본 클래스에서 한 번만 열고 하위 클래스에서 닫았습니다. 코드를 데스크톱 응용 프로그램으로 옮길 때 문제가 발생했습니다. 위에서 언급 한 많은 솔루션을 시도했습니다. GC.Collect()제안 된대로 두 번 이미 구현되었습니다.

그런 다음 Excel을 여는 코드를 하위 클래스로 이동하기로 결정했습니다. 한 번만 여는 대신 이제 새 객체 (기본 클래스)를 만들고 모든 항목에 대해 Excel을 열고 마지막에 닫습니다. 성능에 약간의 불이익이 있지만 여러 테스트를 기반으로 Excel 프로세스가 문제없이 종료되고 (디버그 모드에서) 임시 파일도 제거됩니다. 테스트를 계속하고 업데이트를 받으면 더 작성하겠습니다.

결론은 다음과 같습니다. 특히 클래스가 많은 경우 초기화 코드를 확인해야합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.