已弃用的命名信号量未被释放

9
当一个C#程序持有一个命名信号量时,当应用程序被提前终止(例如通过按Ctrl+C或关闭控制台窗口),它似乎并没有释放。至少在所有进程实例终止之前都没有释放。
使用命名互斥体的情况下,在这种情况下会引发AbandonedMutexException异常,但是在信号量中不会。如何防止一个程序实例在另一个程序实例被提前终止时停顿?
class Program
{
    // Same with count > 1
    private static Semaphore mySemaphore = new Semaphore(1, 1, "SemaphoreTest");

    static void Main(string[] args)
    {
        try
        {
            // Blocks forever if the first process was terminated
            // before it had the chance to call Release
            Console.WriteLine("Getting semaphore");
            mySemaphore.WaitOne();  
            Console.WriteLine("Acquired...");
        }
        catch (AbandonedMutexException)
        {
            // Never called!
            Console.WriteLine("Acquired due to AbandonedMutexException...");
        }
        catch (System.Exception ex)
        {
            Console.WriteLine(ex);
        }

        Thread.Sleep(20 * 1000);
        mySemaphore.Release();
        Console.WriteLine("Done");
    }
}

2
信号量没有所有者。不存在信号量的放弃。如果您希望在线程退出时自动释放,请使用互斥锁。 - Raymond Chen
@George:请查看我的更新答案,其中显示了如何在用户关闭窗口时获得通知。 - Jim Mischel
当你像这样进行交互时,永远不能忽略进程中止。有很少情况下是有意义的继续执行,很多隐式状态已经消失。使用Process.Exited事件打破僵局。 - Hans Passant
2个回答

6
一般情况下,不能保证线程在退出时释放信号量。您可以编写try / finally块和关键终结器,但如果程序异常终止,则不总是起作用。并且,与互斥量不同,如果线程仍然持有信号量而退出,则其他线程不会收到通知。
原因是Windows信号量对象(.NET Semaphore对象基于其上),不跟踪已获取它的线程,因此无法引发类似于AbandonedMutexException的异常。
也就是说,当用户关闭窗口时,您可以被通知。您需要设置控制处理程序以侦听特定事件。调用Windows API函数SetConsoleCtrlHandler,将其传递给处理您感兴趣的事件的回调函数(委托)。我已经有一段时间没有这样做了,但是一般来说。

创建一个托管原型,用于SetConsoleCtrlHandler函数和回调函数:

/// <summary>
/// Control signals received by the console control handler.
/// </summary>
public enum ConsoleControlEventType: int
{
    /// <summary>
    /// A CTRL+C signal was received, either from keyboard input or from a
    /// signal generated by the GenerateConsoleCtrlEvent function.
    /// </summary>
    CtrlC = 0,
    /// <summary>
    /// A CTRL+BREAK signal was received, either from keyboard input or from
    /// a signal generated by GenerateConsoleCtrlEvent.
    /// </summary>
    CtrlBreak = 1,
    /// <summary>
    /// A signal that the system sends to all processes attached to a console
    /// when the user closes the console (either by clicking Close on the console
    /// window's window menu, or by clicking the End Task button command from
    /// Task Manager).
    /// </summary>
    CtrlClose = 2,
    // 3 and 4 are reserved, per WinCon.h
    /// <summary>
    /// A signal that the system sends to all console processes when a user is logging off. 
    /// </summary>
    CtrlLogoff = 5,
    /// <summary>
    /// A signal that the system sends to all console processes when the system is shutting down. 
    /// </summary>
    CtrlShutdown = 6
}

/// <summary>
/// Control event handler delegate.
/// </summary>
/// <param name="CtrlType">Control event type.</param>
/// <returns>Return true to cancel the control event.  A return value of false
/// will terminate the application and send the event to the next control
/// handler.</returns>
public delegate bool ConsoleCtrlHandlerDelegate(ConsoleControlEventType CtrlType);

[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool SetConsoleCtrlHandler(
ConsoleCtrlHandlerDelegate HandlerRoutine,
bool Add);

现在,创建您的处理程序方法:
private static bool ConsoleCtrlHandler(ConsoleControlEventType CtrlType)
{
    switch (CtrlType)
    {
        case CtrlClose:
            // handle it here
            break;
        case CtrlBreak:
            // handle it here
            break;
    }
    // returning false ends up calling the next handler
    // returning true will prevent further handlers from being called.
    return false;
}

最后,在初始化期间,您需要设置控件处理程序:

SetConsoleCtrlHandler(ConsoleControlHandler);

您的控制处理程序现在将在用户关闭窗口时被调用。这将允许您释放信号量或进行其他清理。
您可能会对我的ConsoleDotNet包感兴趣。我写了三篇关于这个东西的文章,其中最后两篇仍然可以在DevSource上找到。我不知道第一篇发生了什么事。

99.999% 的时间,Critical Finalizers 是在正常(和意外)终止时清理内核对象的适当机制。在其他0.001% 的情况下,您可能需要重新启动、重新格式化或更换有问题的计算机,而不是为“不稳定的环境”编写“防御性代码”。http://msdn.microsoft.com/en-us/library/system.runtime.constrainedexecution.criticalfinalizerobject.aspx - Shaun Wilson

2
你可以在类的析构函数中编写 mySemaphore.Release()。
class Program
{
    ~Program()  // destructor
    {
        mySemaphore.Release();
    }
}

或者在您的try\catch中添加finally语句块。
try{}
catch{}
finally 
{
    mySemaphore.Release();
}

如果你正在使用asp.net,你也可以使用位于Global.asax.cs中的Application_End。
protected void Application_End(Object sender, EventArgs eventArgs)
{
    mySemaphore.Release();
}

2
基本上:不要忽略退出路径,但在停止时进行清理。 - TomTom
1
不幸的是,这两个都没有帮助。没有Program实例,即使有,析构函数也不会在关闭控制台窗口或按下Ctrl+C时被调用。finally块只有在Thread.Sleep抛出异常时才会被执行,而它并没有。我也不使用ASP.net。我还尝试了Console.CancelKeyPress,它可以处理Ctrl+C,但无法处理关闭窗口。AppDomain.CurrentDomain.ProcessExit也没有被调用。 - George
1
终结器(非析构函数)不能保证执行。 - Shaun Wilson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接