当线程使用调度程序并且主线程正在等待线程完成时死锁

时间:2014-06-13 18:40:38

标签: c# wpf multithreading deadlock dispatcher

有人可以解释为什么会造成死锁,以及如何解决?

        txtLog.AppendText("We are starting the thread" + Environment.NewLine);

        var th = new Thread(() =>
        {

            Application.Current.Dispatcher.Invoke(new Action(() => // causes deadlock
            {
                txtLog.AppendText("We are inside the thread" + Environment.NewLine); // never gets printed
                // compute some result...
            }));


        });

        th.Start();
        th.Join(); // causes deadlock
        // ... retrieve the result computed by the thread

解释:我需要我的辅助线程来计算结果,并将其返回到主线程。但是辅助线程还必须将调试信息写入日志;并且日志位于wpf窗口中,因此线程需要能够使用dispatcher.invoke()。但是当我执行Dispatcher.Invoke时,会发生死锁,因为主线程正在等待辅助线程完成,因为它需要结果。

我需要一个模式来解决这个问题。请帮我改写这段代码。 (请写出实际代码,不要只说"使用BeginInvoke")。谢谢。

另外,从理论上讲,我并不了解一件事:只有当两个线程以不同的顺序访问两个共享资源时才会发生死锁。但在这种情况下,实际资源是什么?一个是GUI。但另一个是什么?我看不到它。

并且通常通过强制规则来解决死锁,即线程只能以精确的顺​​序锁定资源 。我已经在其他地方做过这件事了。但是我怎么能在这种情况下强加这个规则,因为我不明白实际的资源是什么?

5 个答案:

答案 0 :(得分:4)

简答:使用abstract代替BeginInvoke()。 很长的答案改变了你的方法:看到了替代方案。

目前您的Invoke()导致主线程被阻塞,等待终止辅助线程,但是辅助线程正在等待主线程执行您的AppendText操作,因此您的应用程序已死锁。

如果更改为Thread.Join(),那么您的第二个线程将不会等到主线程执行您的操作。而不是这样,它将对您的调用进行排队并继续。你的主线程不会在Join()上被阻塞,因为你的第二个线程这次成功结束了。然后,当主线程完成时,此方法将可以自由地将排队的调用处理为AppendText

替代:

BeginInvoke()

答案 1 :(得分:1)

发生此死锁是因为UI线程正在等待后台线程完成,并且后台线程正在等待UI线程变为空闲。

最佳解决方案是使用async

var result = await Task.Run(() => { 
    ...
    await Dispatcher.InvokeAsync(() => ...);
    ...
    return ...;
});

答案 2 :(得分:1)

Dispatcher正在尝试在UI消息循环中执行工作,但同一个循环当前卡在th.Join上,因此它们正在等待彼此并导致死锁。

如果你开始Thread并且立即开始Join,你肯定会有代码味道,应该重新思考你正在做什么。

如果您希望在不阻止用户界面的情况下完成任务,您只需await上的InvokeAsync

答案 3 :(得分:0)

我遇到了类似的问题,我最终以这种方式解决了这个问题:

do{
    // Force the dispatcher to run the queued operations 
    Dispatcher.CurrentDispatcher.Invoke(delegate { }, DispatcherPriority.ContextIdle);
}while(!otherthread.Join(1));

这会产生一个由于其他线程上的GUI操作而无法阻止的Join。

这里的主要技巧是使用空委托阻止Invoke(无操作),但优先级设置小于队列中的所有其他项。这迫使调度员在整个队列中工作。 (默认优先级为DispatcherPriority.Normal = 9,因此我的DispatcherPriority.ContextIdle = 3完全不在。)

Join()调用使用1 ms超时,只要连接不成功,就会重新清空调度程序队列。

答案 4 :(得分:0)

我真的很喜欢@ user5770690的答案。我创建了一种扩展方法,可以保证在调度程序中继续“抽取”或处理,并避免这种死锁。我稍微改了一下,但效果很好。我希望它可以帮助别人。

    public static Task PumpInvokeAsync(this Dispatcher dispatcher, Delegate action, params object[] args)
    {
        var completer = new TaskCompletionSource<bool>();

        // exit if we don't have a valid dispatcher
        if (dispatcher == null || dispatcher.HasShutdownStarted || dispatcher.HasShutdownFinished)
        {
            completer.TrySetResult(true);
            return completer.Task;
        }

        var threadFinished = new ManualResetEvent(false);
        ThreadPool.QueueUserWorkItem(async (o) =>
        {
            await dispatcher?.InvokeAsync(() =>
            {
                action.DynamicInvoke(o as object[]);
            });
            threadFinished.Set();
            completer.TrySetResult(true);
        }, args);

        // The pumping of queued operations begins here.
        do
        {
            // Error condition checking
            if (dispatcher == null || dispatcher.HasShutdownStarted || dispatcher.HasShutdownFinished)
                break;

            try
            {
                // Force the processing of the queue by pumping a new message at lower priority
                dispatcher.Invoke(() => { }, DispatcherPriority.ContextIdle);
            }
            catch
            {
                break;
            }
        }
        while (threadFinished.WaitOne(1) == false);

        threadFinished.Dispose();
        threadFinished = null;
        return completer.Task;
    }