20

Sometimes when I end the application and it tries to release some COM objects, I receive a warning in the debugger:

RaceOnRCWCleanUp was detected

If I write a class which uses COM objects, do I need to implement IDisposable and call Marshal.FinalReleaseComObject on them in IDisposable.Dispose to properly release them?

If Dispose is not called manually then, do I still need to release them in the finalizer or will the GC release them automatically? Now I call Dispose(false) in the finalizer but I wonder if this is correct.

The COM object I use also have an event handler which the class listens to. Apparently the event is raised on another thread, so how do I correctly handle it if it is fired when disposing the class?

Community
  • 1
  • 1
Alvin Wong
  • 12,210
  • 5
  • 51
  • 77
  • 3
    That is a reminder that manual memory management isn't such a great idea. That MDA warns because of potential hard crashes in the COM server. Trying to dispose anything at app shutdown is pointless, it will be finalized a millisecond later. The GC already knows how to do this, avoid helping. – Hans Passant Mar 31 '13 at 11:24

3 Answers3

18

First - you never have to call Marshal.ReleaseComObject(...) or Marshal.FinalReleaseComObject(...) when doing Excel interop. It is a confusing anti-pattern, but any information about this, including from Microsoft, that indicates you have to manually release COM references from .NET is incorrect. The fact is that the .NET runtime and garbage collector correctly keep track of and clean up COM references.

Second, if you want to ensure that the COM references to an out-of-process COM object is cleaned up when your process ends (so that the Excel process will close), you need to ensure that the Garbage Collector runs. You do this correctly with calls to GC.Collect() and GC.WaitForPendingFinalizers(). Calling twice is safe, end ensures that cycles are definitely cleaned up too.

Third, when running under the debugger, local references will be artificially kept alive until the end of the method (so that local variable inspection works). So a GC.Collect() calls are not effective for cleaning object like rng.Cells from the same method. You should split the code doing the COM interop from the GC cleanup into separate methods.

The general pattern would be:

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

There is a lot of false information and confusion about this issue, including many posts on MSDN and on StackOverflow.

What finally convinced me to have a closer look and figure out the right advice was this post https://blogs.msdn.microsoft.com/visualstudio/2010/03/01/marshal-releasecomobject-considered-dangerous/ together with finding the issue with references kept alive under the debugger on some StackOverflow answer.

Govert
  • 16,387
  • 4
  • 60
  • 70
  • thanks for your comment, but how exactly should I use your suggestion - from what I see "DotheWork" is a Sub where you do things with Excel, and "WrapperThatCleansUp" should clean COM objects - but from which point do you decide to call this Sub ? If you put It inside where you do stuff with Excel(like 2DoTheWork" Sub) then you're catched in a loop. – LuckyLuke82 Jul 04 '17 at 16:37
  • 'WrapperThatCleansUp' does two things: (1) Call the Sub that makes the COM calls, and (2) Calls the Garbage Collector to clean up. Higher-level code would call 'WrapperThatCleansUp'. Like this example, you should put the COM work in a separate routine to the one where you call the GC, to ensure a clear scope of the local variables. – Govert Jul 05 '17 at 10:04
  • that is absolutely awesome - best solution ever ! I was actually in one of Microsoft courses recently and what they teach and recommend regarding this issue is quite different and longer in code by any means. What they recommend is this: 1.) create a class that implements IDisposable interface and write methods for COM object inside (for creating document, saving etc.). 2.) call that class from whenever you want with Using Statement - and that should take care of object being disposed....But, your code is best I've seen and shortest so thanks a lot ! – LuckyLuke82 Jul 05 '17 at 11:16
  • just to mention - your code works but for me only when I include Marshal.ReleaseComObject() in my code too - am I doing something wrong and did you really test your code to be 100% sure ? – LuckyLuke82 Jul 05 '17 at 12:50
  • I've tested it many times, and checked again just now like this in VS 2017: 1. Create a new VB.NET Project (Windows Forms, .NET 4.) 2. Add a reference to the Excel COM library 3. Add a button to the form. 4. Paste the code above and call WrapperThatCleanUp() from the button handler. 5. Press the button and check that Excel pops up and closes and disappears completely from the Task Manager. Can you try these exact steps? Doing Marshal.ReleaseComObject really is not required - read the Microsoft link in my answer to understand better. – Govert Jul 05 '17 at 13:22
  • I tried to declare a variable that holds Excel.Apliccation object at top of the class, but then It didn't work - with this I also noticed that Excel process get's started immediately when you open form. It didn't work neither If I assigned what this variable is only in the sub where all the work is done. So I moved entire code of Excel into same sub, as your "DoTheWork" and now It works fine. – LuckyLuke82 Jul 05 '17 at 13:55
  • 1
    Yes, a class-level reference will keep Excel alive unless you explicitly set it to 'Nothing' before you call the GC. – Govert Jul 05 '17 at 14:44
  • I didn't think of that. Thanks, I will try again:) – LuckyLuke82 Jul 05 '17 at 16:03
  • You say running under the debugger, but if it's a windows service, does that refer to having a debugger attached to a service compiled in debug mode, or is compiling it in debug mode enough to keep the COM references artificially alive even without the debugger being attached to the service? – Markus May 17 '18 at 07:55
  • AFAIK, the references are kept alive until the end of the method when a debugger is attached to the method - it's not that code compiled with the Debug flag looks different. You could test this by doing GC and then a long sleep (after the last reference is cleared). – Govert May 17 '18 at 08:21
  • What about the article provided by James that states "GC.Collect will stop all .NET process in the OS to collect garbage. With each call to GC.Collect, you may promote garbage from Generation 0 to Generation 1 and then to Generation 2. Once your garbage reaches Generation 2, you must terminate your process to recover memory". Is that not a concern when you're hammering GC.Collect? – Murphybro2 Apr 23 '19 at 14:26
  • @Murphybro2 Neither of those statements are correct. Garbage collection does not stop all processes, it only stops the threads in the current process. Gen 2 is reclaimed when a full garbage collection is run. (See e.g. here: https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals ). Finally (!) the idea is not to 'hammer' the GC, but to make sure it runs at least once after the COM work talking to Excel is complete. This ensures a clean-up of the COM objects without explicit COM reference management. – Govert Sep 11 '19 at 15:08
  • I have seen this method in some sample codes from Dassault Systems for SolidWORKS – Hamed Sep 23 '20 at 23:50
  • @Govert Although this solution is much easier for short lived interop, I'm not sure I agree with this approach in a VSTO setting. If you have very long-running sessions, will you not keep accumulating COM-object objects for the duration of the session, i.e. a memory leak? – Alexander Høst Feb 09 '22 at 17:16
  • @AlexanderHøst If memory is tight, the GC will run and collect. It probably runs now-and-then anyway (when Gen 2 is full etc.). My perspective is that if you're worried about keeping onto COM objects too long, you can force a GC from time to time, rather than try to do your own reference tracking. Especially easy if you have some chunk of work (say writing and formatting a sheet) and when it is done you want to clean up. Manual reference tracking is extremely messy and IMHO not needed here. – Govert Feb 09 '22 at 17:38
  • @Govert I just find this conclusion so strange. We've tried numerous approaches to this over the years (including your suggestion), but the only solution we've found where we are able to consistently avoid both performance degradation and unresponsive Office apps is when we manually release all COM references (so much so that we've built a framework around it that makes it impossible to create leaks in the first place - with a single top-level Dispose). I'm not saying you're wrong necessarily (it's a great post), we just haven't been able to achieve the results we want with this approach. – Alexander Høst Feb 09 '22 at 17:54
16

Based on my experience using different COM objects (in-process or out-of-process) I would suggest one Marshal.ReleaseComObject per one COM/ .NET boundary crossing (if for an instance you reference COM object in order to retrieve another COM reference).

I have run into many issues just because I decided to postpone COM interop cleanup to GC. Also please notice, I never use Marshal.FinalReleaseComObject - some COM objects are singletons and it doesn't work well with such objects.

Doing anything in managed objects inside finalizer (or Dispose(false) from the well-known IDisposable implementation) is forbidden. You must not rely on any .NET object reference in the finalizer. You can release IntPtr, but not COM object as it could already be clean up.

smead
  • 1,768
  • 15
  • 23
aguyngueran
  • 1,301
  • 10
  • 23
  • What if my class which uses the COM object will exist till application terminates? And if the "documentation" of the COM objects "requires" calling some kind of `Deactivate` methods? (I am not sure what it does, but I tried not calling it and nothing bad happens.) – Alvin Wong Mar 31 '13 at 15:17
12

There's an article here on that: http://www.codeproject.com/Tips/235230/Proper-Way-of-Releasing-COM-Objects-in-NET

In a nutshell:

1) Declare & instantiate COM objects at the last moment possible.
2) ReleaseComObject(obj) for ALL objects, at the soonest moment possible.
3) Always ReleaseComObject in the opposite order of creation.
4) NEVER call GC.Collect() except when required for debugging.

Until GC naturally occurs, the com reference will not be fully released. This is why so many people need to force object destruction using FinalReleaseComObject() and GC.Collect(). Both are required for dirty Interop code.

Dispose is NOT automatically called by the GC. When an object is being disposed, the destructor is called (in a different thread). This is usually where you could release any unmanaged memory, or com references.

Destructors: http://msdn.microsoft.com/en-us/library/66x5fx1b.aspx

... when your application encapsulates unmanaged resources such as windows, files, and network connections, you should use destructors to free those resources. When the object is eligible for destruction, the garbage collector runs the Finalize method of the object.

James Wilkins
  • 6,836
  • 3
  • 48
  • 73
  • Why we shouldn't call GC.Collect() following up by GC.WaitForPendingFinalizers()? – Martin Braun Jan 13 '16 at 13:00
  • I'm not 100% sure myself, but if you read the article it says "GC.Collect will stop all .NET process in the OS to collect garbage. With each call to GC.Collect, you may promote garbage from Generation 0 to Generation 1 and then to Generation 2. Once your garbage reaches Generation 2, you must terminate your process to recover memory." So, perhaps it's because of the potential for COM objects to get stuck in the GC for too long by forcing too many GC calls. – James Wilkins Jan 13 '16 at 22:30
  • It's more that frequent calls to GC.Collect will prevent the GC from freeing up memory in a timely manner. This is because long lived objects (i.e. generation 2) are checked less frequently by the GC; the trouble is that the generation of an object is promoted every time the GC skips them (i.e. a reference to the object still exists), so by calling GC.Collect object are prematurely promoted, thus tricking the GC into thinking they are long lived even if they aren't. – jonvw Jul 14 '16 at 19:36
  • If effect you said the EXACT same thing I did, minus the part about possibly needing to terminate your app (which in many cases I find this to be true). I certainly hope you didn't downvote the answer because you disagreed with my comment. If you have issue with the answer itself, explain. If it wasn't you, then whoever did, reading this now, should. – James Wilkins Jul 15 '16 at 04:41