'死锁'只有一个被锁定的物体?

时间:2012-01-28 14:21:59

标签: c# .net winforms multithreading locking

我在C#中遇到多线程问题。 我使用一个事件来更新另一个线程的表单中的标签,当然我需要使用Invoke()命令。 那部分也很好。 但是,用户可以关闭表单,如果在不幸的时间发送事件,程序可能会崩溃。

所以,我想我会简单地重写表单的Dispose()方法,在锁定代码中将boolean设置为true,并检查该布尔值并在锁定代码中调用该事件。

但是,每次关闭表单时,程序都会完全冻结。

以下是代码中提到的部分:

private object dispose_lock = new object();
private bool _disposed = false;

private void update(object sender, EventArgs e)
{
    if (InvokeRequired)
    {
        EventHandler handler = new EventHandler(update);
        lock (dispose_lock)
        {
            if (_disposed) return;
            Invoke(handler); // this is where it crashes without using the lock
        }
        return;
    }

    label.Text = "blah";
}

protected override void Dispose(bool disposing)
{
    eventfullObject.OnUpdate -= update;
    lock (dispose_lock) // this is where it seems to freeze
    {
       _disposed = true; // this is never called
    }
    base.Dispose(disposing);
}

我希望这里的任何人都知道这段代码有什么问题。 提前谢谢!

7 个答案:

答案 0 :(得分:6)

您没有考虑的是传递给Invoke的委托在UI线程上异步调用。调用Invoke会将消息发布到表单消息队列,并在以后的某个时间内被提取。

不会发生什么:

UI Thread                   Background Thread
                            Call update()
                            take lock
                            Call Invoke()
Call update()             
                            release lock
Call Dispose()
take lock
release lock

但相反:

UI Thread                   Background Thread
                            Call update()
                              take lock
                               Call Invoke()
                               block until UI Thread processes the message
Process messages
...
Dispose() 
   wait for lock ****** Deadlock! *****
...
Call update()             
                            release lock

因此,后台线程可以在UI线程尝试运行时保持锁定Dispose

解决方案比您尝试的解决方案简单得多。由于Invoke是异步发布的,因此不需要锁定。

private bool _disposed = false;

private void update(object sender, EventArgs e)
{
    if (InvokeRequired)
    {
        EventHandler handler = new EventHandler(update);
        Invoke(handler);
        return;
    }

    if (_disposed) return;

    label.Text = "blah";
}

protected override void Dispose(bool disposing)
{
    eventfullObject.OnUpdate -= update;
    _disposed = true; // this is never called
    base.Dispose(disposing);
}

只有在UI线程上读取或写入_disposed标志,因此不需要锁定。现在你调用堆栈看起来像:

UI Thread                   Background Thread
                            Call update()
                               take lock
                               Call Invoke()
                               block until UI Thread processes the message
Process messages
...
Dispose() 
  _disposed = true;
...

Call update()
  _disposed is true so do nothing             

答案 1 :(得分:1)

我真的很简单。而不是实现棘手的线程安全代码,我只是捕获异常,如果失败则什么也不做。

假设它是ObjectDisposedException

try
{
    this.Invoke(Invoke(handler));
}
catch (ObjectDisposedException)
{
    // Won't do anything here as
    // the object is not in the good state (diposed when closed)
    // so we can't invoke.
}

它更简单,更直接。如果注释指定为什么你捕获异常,我认为没关系。

答案 2 :(得分:1)

使用Control.Invoke的一个危险是它可能会在您建议的不幸时间处理在UI线程上。最常见的方式是当您有以下事件顺序时

  1. 后台线程:使用Invoke
  2. 对回调进行排队
  3. 前景线程:释放名为Invoke的背景的控件
  4. 前景线程:在已处置的控件
  5. 上将回调列出

    在这种情况下,Invoke将失败并导致在后台线程上引发异常。这可能是导致您的应用程序崩溃的原因。

    使用新代码会导致死锁。代码将在步骤#1中进行锁定。然后在步骤#2的UI中发生处置,它正在等待锁定,直到步骤#3完成后才会释放锁定。

    解决此问题的最简单方法是接受Invoke是一项可能会失败的操作,因此需要try / catch

    private void update(object sender, EventArgs e)
    {
        if (InvokeRequired)
        {
            EventHandler handler = new EventHandler(update);
            try 
            {
                Invoke(handler); 
            }
            catch (Exception) 
            {
                // Control disposed while invoking.  Nothing to do 
            }
            return;
        }
    
        label.Text = "blah";
    }
    

答案 3 :(得分:1)

为什么不使用BeginInvoke而不是Invoke - 这不会阻止后台线程。看起来没有任何具体原因后台线程需要等待UI更新从您显示的内容发生

答案 4 :(得分:1)

  

调用Dispatcher.Invoke(in。)时会出现另一个死锁情况   WPF应用程序)或Control.Invoke(在Windows窗体应用程序中)   而拥有一把锁。如果UI碰巧正在运行另一个   正在等待同一个锁的方法,就会发生死锁   那里。这通常可以通过调用BeginInvoke来解决   Invoke。或者,您可以在致电前释放锁   调用,虽然如果你的呼叫者取出锁定,这将无法工作。我们   解释富客户端应用程序和线程中的Invoke和BeginInvoke   亲和力。

来源:http://www.albahari.com/threading/part2.aspx

答案 5 :(得分:0)

IMO Dispose为时已晚......

我建议将一些代码放入FormClosing中,在Dispose AFAIK发生之前调用该代码。

对于这种情况,我通常倾向于使用不同的(原子)模式进行检查 - 例如通过Interlocked类。

private long _runnable = 1;

private void update(object sender, EventArgs e)
{
    if (InvokeRequired)
    {
        EventHandler handler = new EventHandler(update);
        if (Interlocked.Read (ref _runnable) == 1) Invoke(handler);
        return;
    }

    label.Text = "blah";
}

FormClosing中,您只需拨打Interlocked.Increment (ref _runnable)

答案 6 :(得分:0)

只是因为没有其他答案是罪魁祸首,是否有其他代码终止未发布的线程?我想你可能正在使用普通的Threads而不是BackgroundWorker,可能忘记将Thread.isBackround设置为true