3

当 Windows 关闭时,我试图优雅地关闭我的 vb.net 控制台应用程序。我找到了调用 Win32 函数 SetConsoleCtrlHandler 的示例,它们基本上看起来像这样:

Module Module1

Public Enum ConsoleEvent
    CTRL_C_EVENT = 0
    CTRL_BREAK_EVENT = 1
    CTRL_CLOSE_EVENT = 2
    CTRL_LOGOFF_EVENT = 5
    CTRL_SHUTDOWN_EVENT = 6
End Enum

Private Declare Function SetConsoleCtrlHandler Lib "kernel32" (ByVal handlerRoutine As ConsoleEventDelegate, ByVal add As Boolean) As Boolean
Public Delegate Function ConsoleEventDelegate(ByVal MyEvent As ConsoleEvent) As Boolean


Sub Main()

    If Not SetConsoleCtrlHandler(AddressOf Application_ConsoleEvent, True) Then
        Console.Write("Unable to install console event handler.")
    End If

    'Main loop
    Do While True
        Threading.Thread.Sleep(500)
        Console.WriteLine("Main loop executing")
    Loop

End Sub


Public Function Application_ConsoleEvent(ByVal [event] As ConsoleEvent) As Boolean

    Dim cancel As Boolean = False

    Select Case [event]

        Case ConsoleEvent.CTRL_C_EVENT
            MsgBox("CTRL+C received!")
        Case ConsoleEvent.CTRL_BREAK_EVENT
            MsgBox("CTRL+BREAK received!")
        Case ConsoleEvent.CTRL_CLOSE_EVENT
            MsgBox("Program being closed!")
        Case ConsoleEvent.CTRL_LOGOFF_EVENT
            MsgBox("User is logging off!")
        Case ConsoleEvent.CTRL_SHUTDOWN_EVENT
            MsgBox("Windows is shutting down.")
            ' My cleanup code here
    End Select

    Return cancel ' handling the event.

End Function

这工作正常,直到我在收到此异常时将其合并到现有程序中:

检测到 CallbackOnCollectedDelegate 消息:对“AISLogger!AISLogger.Module1+ConsoleEventDelegate::Invoke”类型的垃圾收集委托进行了回调。这可能会导致应用程序崩溃、损坏和数据丢失。将委托传递给非托管代码时,托管应用程序必须使它们保持活动状态,直到保证它们永远不会被调用。

大量搜索表明问题是由未引用的委托对象引起的,因此超出了范围,因此被垃圾收集器处理掉了。这似乎可以通过在上面示例中的主循环中添加 GC.Collect 来确认,并在关闭控制台窗口或按 ctrl-C 时获得相同的异常。问题是,我不明白“引​​用代表”是什么意思?这对我来说听起来像是将变量分配给函数???我怎样才能在VB中做到这一点?有很多这样的 C# 示例,但我无法将它们翻译成 VB。

谢谢。

4

3 回答 3

3
    If Not SetConsoleCtrlHandler(AddressOf Application_ConsoleEvent, True) Then

这是让你陷入困境的陈述。它即时创建一个委托实例并将其传递给非托管代码。但是垃圾收集器看不到该非托管代码持有的引用。您需要自己存储它,这样它就不会被垃圾收集。让它看起来像这样:

Private handler As ConsoleEventDelegate

Sub Main()
    handler = AddressOf Application_ConsoleEvent
    If Not SetConsoleCtrlHandler(handler, True) Then
       '' etc...

处理程序变量现在保持它被引用,并在程序的生命周期内这样做,因为它是在模块中声明的。

顺便说一句,您无法取消关机,但这是另一个问题。

于 2013-03-09T23:53:17.030 回答
1

指向函数的指针在 .Net 中表示为委托。委托是一种对象,与任何其他对象一样,如果没有对它的引用,那么它将被垃圾回收。

表达式(AddressOf Application_ConsoleEvent)创建委托。 SetConsoleCtrlHandler是一个原生函数,所以它不理解委托;它接收一个原始函数指针。所以操作顺序是:

  1. (AddressOf Application_ConsoleEvent)创建委托。
  2. SetConsoleCtrlHandler接收指向委托的原始函数指针,并存储原始指针以供以后使用。
  3. 时间流逝。请注意,现在没有任何内容引用委托。
  4. 发生垃圾收集并收集委托,因为它未被引用。
  5. 应用程序正在关闭,因此 Windows 尝试通过调用原始函数指针来通知您。这指向一个不存在的委托,所以你崩溃了。

您需要声明一个变量以保留对委托的引用。我的 VB 很生锈,但类似于:

Shared keepAlive As ConsoleEventDelegate

然后

keepAlive = AddressOf ConsoleEventDelegate
If Not SetConsoleCtrlHandler(keepAlive, True) Then
    'etc.
于 2013-03-09T23:54:12.147 回答
0

执行此操作的最简单方法可能是处理AppDomain.ProcessExit 事件,该事件在应用程序的父进程退出时引发。

    For example:
        Module MyApp

        Sub Main()
            ' Attach the event handler method
            AddHandler AppDomain.CurrentDomain.ProcessExit, AddressOf MyApp_ProcessExit

            ' Do something
            ' ...

            Environment.Exit(0)
        End Sub

        Private Sub MyApp_ProcessExit(sender As Object, e As EventArgs)
            Console.WriteLine("App Is Exiting...")
        End Sub

    End Module

但是调用Environment.Exit可能不是解决原始问题的最佳方法。通常,唯一需要使用此方法的情况是可能有其他前台线程正在运行。在这种情况下,值得研究如何优雅地终止那些其他线程,而无需采取严厉措施来杀死整个进程。

Environment.Exit,尽管名字听起来有点好听,但却是一个相当残酷的措施。它不像在 Windows 任务管理器中单击“结束任务”那么糟糕(请注意,如果您这样做,不会引发ProcessExit事件,这意味着上述建议将不起作用),但这可能不是您的解决方案真的很想要,要么。

于 2014-06-06T05:00:31.720 回答