在STA线程中创建的COM RWC在线程终止时断开与底层COM对象的连接是否正常?

时间:2017-03-29 02:54:47

标签: .net com

首先,我想说清楚这不是关于如何在.Net下发布COM对象的问题。这是一个关于尝试理解可能由于正常的COM行为导致的意外COM对象释放的问题,但是我无法找到对观察到的行为的明确解释,并且希望确认我基于某些做出的推断报价稍后提出。

我注意到,当通过COM-Interop使用Excel时,Excel实例将完全终止,正如人们所希望的那样,当在Apartmentstate设置为ApartmentState.STA的辅助线程中创建互操作引用时。通过使用Marshal.ReleaseCOMObject显式调用它们的释放或通过调用垃圾收集器(GC)来按顺序清理对象,不会采取任何操作来清除运行时可调用包装器(RCW' s)上的引用计数。用于Excel完全关闭。对于那些不熟悉使用Excel Interop的人,请注意,众所周知,在被告知退出所有.Net COM参考文件后,它不会关闭。

我的第一个想法是GC在线程完成时自动运行。为了确定这是否属实,我使用了Visual Studio"性能和诊断"监视内存使用情况的工具。

enter image description here

上图,我首先运行在UI线程上与Excel交互然后在MTA线程上交互的方法。可以观察到,在运行GC以释放COM引用之前,Excel进程不会终止。请注意,GC标记位于分析图表上。然后我在STA线程上运行该方法两次。可以观察到Excel进程终止而不需要任何其他操作,并且性能分析图表指示在Excel启动的线程退出后GC没有运行。此外,如果我尝试在终止后访问在STA线程中创建的引用,则不能使用已与其基础RCW分离的" COM对象。"抛出异常。

此时我认为Excel进程的发布在某种程度上与用于创建对象的线程的回收有关。我在STA线程上运行了两次执行Excel方法的程序并记录下面显示的结果。可以看出,在测试的整个生命周期中,所有线程实例以及COM对象都被列为活动。

enter image description here

在研究COM对象的生命周期中,我在Larry Osterman的博客文章中发现了以下声明"这些“Threading Models” and why do I care?"似乎解释了为什么.Net RCW&# 39; s与底层COM对象断开连接。

  

COM对象的生命周期仅限于创建对象的公寓的生命周期。因此,如果您在STA中创建对象,然后销毁公寓(通过调用CoUninitialize),则将销毁在此公寓中创建的所有对象。

这句话暗示STA COM公寓是控制机制。但是,我发现的唯一表明公寓生命周期对.Net对象的影响的是Chris Brumme博客文章"Apartments and Pumping in the CLR"中的以下引文。

  

我们的COM Interop层确保我们几乎只在正确的公寓和上下文中调用COM对象。我们违反COM规则的地方是COM对象的公寓或上下文被拆除的时候。在这种情况下,我们仍然会在pUnk上调用IUnknown :: Release来尝试恢复其资源,即使这是严格违法的。

最后我的问题是:我观察到的是在线程执行结束时为线程被破坏而创建的STA公寓的结果,从而允许Excel进程终止,因为不再有任何对象持有对它的引用?

我最初声明这不是关于如何在.Net中发布COM对象的问题。但是,我很欣赏使用这种技术可能产生负面影响的任何见解。它一直有效,但是当文档技术很容易实现时,我对使用它犹豫不决。

下面介绍的代码是我用来调查此行为的代码。

Imports System
Imports Excel = Microsoft.Office.Interop.Excel
Imports System.Threading
Imports System.Runtime.InteropServices
Imports System.Windows.Forms
Imports System.Diagnostics

Public Class frmComRelease : Inherits Form
    Private launchedExcelProcesses As New System.Collections.Concurrent.ConcurrentDictionary(Of Process, ApartmentState) ' requires Proj Ref: System.ServiceModel.dll
    Private btnRunUI As Button
    Private btnRunMTA As Button
    Private btnRunSTA As Button
    Private btnRunGC As Button
    Private btnTryToAccessExcelReference As Button
    Private excelReference As Object
    Private processStatus As TextBox
    Private chkBxGrabReference As CheckBox
    Private grabReference As Boolean
    Private key As New Object

    Public Sub New()
        MyBase.New()
        Font = New Drawing.Font(Font.FontFamily, 12, Font.Style, Drawing.GraphicsUnit.Pixel)
        Width = 400 : Height = 350

        btnRunUI = AddButton("Run Excel On UI Thead", Nothing, AddressOf btnRunUI_Click)
        btnRunMTA = AddButton("Run Excel On MTA Thead", btnRunUI, AddressOf btnRunMTA_Click)
        btnRunSTA = AddButton("Run Excel On STA Thead", btnRunMTA, AddressOf btnRunSTA_Click)
        btnTryToAccessExcelReference = AddButton("Access Last Excel Reference", btnRunSTA, AddressOf btnTryToAccessExcelReference_Click)
        btnRunGC = AddButton("Run GC to free UI or MTA started Excel Process", btnTryToAccessExcelReference, AddressOf btnRunGC_Click)
        processStatus = New TextBox With {.Multiline = True, .Location = New System.Drawing.Point(5, btnRunGC.Bottom + 10), .Width = Me.ClientSize.Width - 10, .Anchor = AnchorStyles.Bottom Or AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.Top, .ReadOnly = True, .ScrollBars = ScrollBars.Vertical}
        processStatus.Height = ClientSize.Height - processStatus.Top - 5
        Controls.Add(processStatus)
        chkBxGrabReference = New CheckBox() With {.Text = "Hold Excel Reference", .AutoCheck = True, .Location = New System.Drawing.Point(10 + btnRunMTA.Width, 5), .TextAlign = System.Drawing.ContentAlignment.MiddleLeft, .AutoSize = True}
        AddHandler chkBxGrabReference.CheckedChanged, AddressOf chkBxGrabReference_CheckedChanged
        Controls.Add(chkBxGrabReference)
        StartPosition = FormStartPosition.Manual
        Location = New Drawing.Point(500, 100)
    End Sub

    Private Sub chkBxGrabReference_CheckedChanged(sender As Object, e As EventArgs)
        SyncLock key
            grabReference = chkBxGrabReference.Checked
        End SyncLock
    End Sub

    Private Function AddButton(text As String, relativeTo As Control, clickHandler As EventHandler) As Button
        Dim btn As New Button() With {.Text = text, .Location = New System.Drawing.Point(5, If(relativeTo Is Nothing, 5, relativeTo.Bottom + 5)), .TextAlign = System.Drawing.ContentAlignment.MiddleLeft, .AutoSize = True}
        AddHandler btn.Click, clickHandler
        Controls.Add(btn)
        Return btn
    End Function

    Protected Overrides Sub OnClosed(e As EventArgs)
        MyBase.OnClosed(e)
        For Each p As Process In Me.launchedExcelProcesses.Keys
            p.Dispose()
        Next
    End Sub

    Private Sub btnTryToAccessExcelReference_Click(sender As Object, e As EventArgs)
        SyncLock key
            If excelReference IsNot Nothing Then
                Dim ptr As IntPtr
                Dim msg As String
                Try
                    ptr = Marshal.GetIUnknownForObject(excelReference)
                    Marshal.Release(ptr)
                    msg = "Sucessfully accessed reference"
                Catch ex As Exception
                    msg = ex.Message
                End Try
                excelReference = Nothing
                MessageBox.Show(msg)
            End If
        End SyncLock
    End Sub

    Private Sub btnRunUI_Click(sender As Object, e As EventArgs)
        ExcelWork()
    End Sub

    Private Sub btnRunMTA_Click(sender As Object, e As EventArgs)
        Dim t As New Thread(AddressOf ExcelWork)
        t.SetApartmentState(ApartmentState.MTA)
        t.Start()
    End Sub

    Private Sub btnRunSTA_Click(sender As Object, e As EventArgs)
        Dim t As New Thread(AddressOf ExcelWork)
        t.SetApartmentState(ApartmentState.STA)
        t.Start()
    End Sub

    Private Sub btnRunGC_Click(sender As Object, e As EventArgs)
        excelReference = Nothing
        Do
            GC.Collect()
            GC.WaitForPendingFinalizers()
        Loop While System.Runtime.InteropServices.Marshal.AreComObjectsAvailableForCleanup
    End Sub

    Private Sub ExcelWork()
        Dim app As Excel.Application = New Excel.Application()
        app.Visible = True
        PositionExcel(app)
        SyncLock key
            If grabReference Then excelReference = app
        End SyncLock

        Dim processId As Int32
        Dim threadID As Int32 = GetWindowThreadProcessId(app.Hwnd, processId)
        Dim proc As Process = Process.GetProcessById(processId)
        proc.EnableRaisingEvents = True
        Dim state As ApartmentState = Thread.CurrentThread.GetApartmentState()
        launchedExcelProcesses.TryAdd(proc, state)
        UpdateStatus(GetProcessStatusMessage(proc))
        AddHandler proc.Exited, AddressOf Process_Exited
        Dim wb As Excel.Workbook = app.Workbooks.Add()
        For Each cell As Excel.Range In DirectCast(wb.Worksheets.Item(1), Excel.Worksheet).Range("A1:H10")
            cell.Value2 = 10
        Next

        wb.Close(False)
        app.Quit()
        UpdateStatus(String.Format("Exiting {0} thread of Excel process [{1}]", state, proc.Id))
    End Sub

    Private Sub PositionExcel(app As Excel.Application)
        Dim r As System.Drawing.Rectangle = Me.Bounds
        ' Excel position/size measured in pts
        Dim pxTopt As Double
        Using g As Drawing.Graphics = CreateGraphics()
            pxTopt = 72.0 / g.DpiX
        End Using
        app.WindowState = Excel.XlWindowState.xlNormal
        app.Top = r.Top * pxTopt
        app.Left = (r.Right) * pxTopt
        app.Width = r.Width * pxTopt
        app.Height = r.Height * pxTopt
    End Sub

    Private Function GetProcessStatusMessage(process As Process) As String
        Dim state As ApartmentState
        launchedExcelProcesses.TryGetValue(process, state)
        Return String.Format("{3} - Excel process [{0}] {1} at {2}", process.Id, If(process.HasExited, "ended", "started"), If(process.HasExited, process.ExitTime, process.StartTime), state)
    End Function

    Private Sub UpdateStatus(msg As String)
        Invoke(New Action(Of String)(AddressOf processStatus.AppendText), msg & Environment.NewLine)
    End Sub

    Private Sub Process_Exited(sender As Object, e As EventArgs)
        Dim proc As Process = DirectCast(sender, Process)
        UpdateStatus(GetProcessStatusMessage(proc))
        Dim state As ApartmentState
        launchedExcelProcesses.TryRemove(proc, state)
        proc.Dispose()
        proc = Nothing
    End Sub

    <DllImport("user32.dll", SetLastError:=True)>
    Private Shared Function GetWindowThreadProcessId(ByVal hwnd As Int32, ByRef lpdwProcessId As Int32) As Int32
    End Function
End Class

修改:可能相关的其他信息:

Don Box; May 1997, Microsoft Systems Journal, Q&A ActiveX/COM

  

......就其本质而言,所有对象都存在于一个过程中。为   进程外服务器,此进程由动态创建   服务控制管理器(SCM),基于服务器的实现   主/ WinMain函数。对于outofproc服务器,服务器实现者在   完全控制过程何时关闭。标准   服务器的WinMain的实现是拥有主线程的   进程等待,直到没有对象有未完成的客户端   服务。这保证了对象的主页&#34;将继续活着   只要有需要。

1 个答案:

答案 0 :(得分:1)

我无法找到一个官方消息来源称,由.NET调用CoUninitialize。但是,我发现了一些东西。下面是一些&#34;堆栈跟踪&#34;来自.NET Core源代码。我无法找到相应的.NET Framework源代码,但我认为它与此没有太大区别。这些并不是通过此代码的唯一途径,并且这些并非所有COM初始化和未初始化的情况,但这足以证明CLR旨在隐式管理COM框架。 / p>

这里有一些值得注意的事情。 Thread:PrepareApartmentAndContextregisters an IInitializeSpy object。该对象watches for the apartment to be shut down并调用ReleaseRCWsInCaches。也可以从其他一些地方调用该方法。在这些兔子洞的某处,您可以找到您寻找的所有信息。