多线程异常和Dispose。为什么Dispose没有被调用?

3
'

using'语句保证对象将调用Dispose方法。但在这个例子中,这并没有发生。而且终结器方法也没有被调用。

为什么会这样?我该如何更改代码以确保在其他线程出现异常时正确处理对象的释放?

'
class Program
{
    static void Main(string[] args)
    {
        Thread th1 = new Thread(ThreadOne);
        Thread th2 = new Thread(ThreadTwo);

        th1.Start();
        th2.Start();

        th1.Join();
        th2.Join();
    }

    static void ThreadOne()
    {
        using (LockedFolder lf = new LockedFolder(@"C:\SomeFodler"))
        {
            // some pay load
            Thread.Sleep(5000);
        }
    }

    static void ThreadTwo()
    {
        // some pay load
        Thread.Sleep(1000);
        throw new Exception("Unexpected exception");
    }
}

public class LockedFolder : IDisposable
{
    private const string FILENAME_LOCK = ".lock-file";
    private bool bLocked = false;

    public string FullPath { private set; get; }

    public LockedFolder(string FullPath)
    {
        this.FullPath = FullPath;
        Lock();
    }

    private void Lock()
    {
        // lock our folder
        Console.WriteLine("Lock " + FullPath);

        //CreateLockFile(Path + FILENAME_LOCK);
        bLocked = true;
    }

    private void UnLock()
    {
        if (!bLocked)
        {
            Console.WriteLine("Already UnLocked " + FullPath);
            return; // already unlocked
        }

        Console.WriteLine("UnLock " + FullPath);

        // unlock our folder
        //DeleteLockFile(Path + FILENAME_LOCK);
        bLocked = false;
    }

    #region IDisposable Members

    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Free managed resources

            }

            // Free unmanaged resource
            UnLock();
        }

        disposed = true;
    }

    ~LockedFolder()
    {
        Dispose(false);
    }

    #endregion
}

输出:

\Visual Studio 2010\Projects\ExceptionExample\ExceptionExample\bin\Debug>ExceptionExample.exe

锁定 C:\SomeFodler

未处理的异常: System.Exception: 意外的异常 在 \visual studio 2010\Projects\ExceptionExample\ExceptionExample\Program.cs 的 ExceptionExample.Program.ThreadTwo() 中,第 36 行 at System.Threading.ThreadHelper.ThreadStart_Context(Object state) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx) at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading.ThreadHelper.ThreadStart()

没有异常的输出:

\Visual Studio 2010\Projects\ExceptionExample\ExceptionExample\bin\Debug>ExceptionExample.exe 锁定 C:\SomeFodler 解锁 C:\SomeFodler


最后器方法也没有被调用。- 您难道没有调用 GC.SuppressFinalize(this); 吗? - bitxwise
你是说因为控制台没有输出你的Unlock方法中的文本,所以你的Dispose方法没有被调用? - bitxwise
当然,UnLock() 方法在这个示例中进行了简化。在实际样例中,该方法会删除真正的文件(Lock() 方法则会创建文件)。我确信在未处理的异常之后,该文件不会被删除。 - Ilya Georgievsky
你记得在它的生日时发送Dispose卡片了吗? - Sandeep Datta
5个回答

4

没有什么是保证的;比如拔掉插头或终止进程都不会尊重using。所有保证的只有在正常执行(包括大多数合理的异常)时会调用Dispose()

在你的情况下,你有一个未处理的线程异常;那是一个进程杀手。一切都不确定,因为你的进程现在病态并且正在被放下(摆脱它的痛苦)。

如果你想让代码表现良好,你必须确保没有进程杀死异常;未处理的线程异常位于列表的顶部。强烈建议在任何线程级别的代码周围使用try/catch


这真的是在后台线程中出现未处理异常的情况吗?父进程也会被终止吗? - Neil Moss
@Neil - 在2.0及以上版本中,是的。这是1.1和2.0之间最大的更改之一(我指现有功能)。在1.1中,您可以处理事件来解除异常,但我认为它遇到了留下损坏状态的问题。任何未处理的异常在线程顶部会导致整个进程被终止。 - Marc Gravell
今天学到了一些东西。谢谢。 - Neil Moss

4
未处理的异常会导致CLR终止进程。在.NET 4.0中,关闭行为略有不同,最终器会在报告异常后运行。但早期版本不会。
您可以通过编写AppDomain.CurrentDomain.UnhandledException事件处理程序来解决此默认行为。记录或报告异常并调用Environment.Exit()。这允许终结器线程运行并调用您的Unlock()方法。
不要依赖此方法,因为有一些令人讨厌的异常,如StackOverflow或FEEE,它们无论如何都会终止进程。某人绊倒电源线或使用Taskmgr.exe关闭进程也是如此。

它起作用了!但在我发布这个问题之前,我尝试使用这个:static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { GC.Collect(); GC.WaitForPendingFinalizers(); },但是它没有起作用。但是你使用了"Environment.Exit(-1);" - 起作用了!谢谢! - Ilya Georgievsky
当“有人绊倒电源线”时,锁定文件会有过期时间。之后它将被删除。 - Ilya Georgievsky
嗯,听起来不太好。只需使用FileStream类和FileShare.None选项打开文件即可。这将锁定文件,无论发生什么情况,操作系统都会始终释放该锁定。 - Hans Passant

1
原因是当您的进程由于未处理的异常而终止时,最终器不会运行。请参见此处以获取更多详细信息。您可以通过在另一个线程的未处理异常处理程序中强制正常关闭进程来强制运行最终器。
.NET Framework 的策略是在发生未处理的异常时几乎什么都不做,因为不清楚进程处于哪种状态。处理最终器可能是不明智的,因为应用程序状态可能会损坏,并且在最终化期间也会出现异常,这也会导致最终器线程崩溃。净效果是只有一些最终器已运行,其余部分则未被处理。这些后续异常使得更难找到应用程序失败的根本原因。
此致, Alois Kraus

0

如果您在BackgroundWorker上运行线程,则抛出的任何异常都将在工作线程中捕获,并作为线程返回的对象的一部分返回。因此,您不必担心异常会逃逸。

这会创建另一个问题,即您无法在BackgroundWorker上调用Join,但是您可以向工作类添加一个计数器设置为0(阻止)的Semaphore

  private Semaphore workerFinsished = new Semaphore(0, 1);

在运行工作程序后添加一个等待。
  public void Join() { workerFinished.WaitOne(); }

在你想要发出完成信号的工作代码中添加一个“Release”。

  workerFinished.Release() 

0
正如Marc所说,一旦您的应用程序出现未处理的异常,所有的赌注基本上都失效了。为了澄清using实际上是在做什么,它会采取这样的代码:
using(var myDisposableObject = GetDisposableObject()) {
    // Do stuff with myDisposableObject
}

并将其翻译成类似于这样的内容:

MyDisposableObject myDisposableObject;
try {
    myDisposableObject = GetDisposableObject();
    // Do stuff with myDisposableObject
}
finally {
    if(myDisposableObject != null) {
        myDisposableObject.Dispose();
    }
}

当你的应用程序遇到未处理的异常时会发生什么?未处理的异常会导致应用程序终止。这种终止(或任何意外的终止)可能会阻止你的using语句中的finally块正确执行。

你应该始终处理你的异常,即使在你没有用try块包围你的线程调用的情况下。考虑挂钩AppDomain.UnhandledException事件来清理资源、记录信息等,在你的应用程序崩溃之前。

编辑

刚刚注意到Hans发布了关于AppDomain.UnhandledException的类似内容,他是对的。在任何程序中,意外终止都可能导致意想不到的结果。然而,在你的情况下,正如建议的那样,不要依赖完整执行你的应用程序完成,特别是在文件资源方面。相反,考虑编写你的进程来预测,甚至预期之前的执行失败。然后,你的应用程序可以根据需要解决不完整的执行。你可以创建日志来跟踪你的过程中的步骤和评估它们的每次运行以解决错误的执行状态。

另外,作为另一个注意事项,您的类(即使考虑到它们只是示例)不是线程安全的...您没有保护共享资源。

非常感谢您的回答,您帮助我理解了为什么会发生这种情况。 - Ilya Georgievsky

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