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

问题描述 投票:0回答:6

有人可以解释为什么这会造成僵局以及如何解决它吗?

        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”)。谢谢。

另外,从理论上讲,我不明白一件事:只有当两个线程以不同的顺序访问两个共享资源时,才会发生死锁。但这种情况下的实际资源是什么?一是图形用户界面。但另一个是什么?我看不到。

死锁通常是通过施加线程只能以精确顺序锁定资源的规则来解决的。我已经在其他地方这样做了。但在这种情况下,我如何强加这条规则,因为我不明白实际的资源是什么?

c# wpf multithreading deadlock dispatcher
6个回答
8
投票

简短回答:使用

BeginInvoke()
而不是
Invoke()
。 长答案改变你的方法:看看替代方案。

当前您的

Thread.Join()
导致主线程被阻塞,等待辅助线程的终止,但辅助线程正在等待主线程执行您的AppendText操作,因此您的应用程序陷入死锁。

如果您更改为

BeginInvoke()
,那么您的辅助线程将不会等到主线程执行您的操作。相反,它会将您的调用排队并继续。您的主线程不会在 Join() 上阻塞,因为这次您的辅助线程成功结束。然后,当主线程完成时,此方法将可以自由处理对 AppendText 的排队调用

替代方案:

void DoSomehtingCool()
{
    var factory = new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext());
    factory.StartNew(() =>
    {
        var result = await IntensiveComputing();
        txtLog.AppendText("Result of the computing: " + result);
    });
}

async Task<double> IntensiveComputing()
{
    Thread.Sleep(5000);
    return 20;
}

2
投票

发生这种死锁是因为 UI 线程正在等待后台线程完成,而后台线程正在等待 UI 线程变得空闲。

最好的解决方案是使用

async
:

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

2
投票

Dispatcher
正在尝试在 UI 消息循环中执行工作,但该循环当前卡在
th.Join
上,因此它们正在互相等待,从而导致死锁。

如果你开始

Thread
并立即
Join
,你肯定有代码味道,应该重新思考你在做什么。

如果您希望在不阻塞 UI 的情况下完成操作,您只需

await
InvokeAsync


1
投票

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

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 毫秒超时,只要加入不成功,就会重新清空调度程序队列。


1
投票

我真的很喜欢@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;
    }

0
投票

虽然人们对

Invoke
InvokeAsync
进行了很多关注,但我认为没有人真正意识到 UI 线程从一开始就不应该
Join
另一个线程。我想不出这样做的正当理由。相反,至少有两种更好的方法,这两种方法都不会导致死锁:

如果您必须创建一个新线程...

        txtLog.AppendText("We are starting the thread" + Environment.NewLine);
        TaskCompletionSource threadComplete = new TaskCompletionSource();
        var th = new Thread(() =>
        {
            Application.Current.Dispatcher.Invoke(new Action(() =>
            {
                txtLog.AppendText("We are inside the thread" + Environment.NewLine); 
            }));
            threadComplete.SetResult();
        });

        th.Start();

        await threadComplete.Task;

        // Continue with life

更好的是,只需使用线程池...

        await Task.Run(() =>
        {
            Application.Current.Dispatcher.Invoke(new Action(() =>
            {
                txtLog.AppendText("We are inside the thread" + Environment.NewLine);
            }));
        });

        // Continue with life

这两种情况都不会导致死锁;无论哪种情况,只要 UI 线程遇到

await
,它就会屈服于队列中的任何内容;最终它将到达您的委托,
Invoke
将返回,
await
ed任务将完成,
// Continue with life
之后的所有事情将继续。

使用同步

Invoke
通常不是问题。
Invoke
阻塞calling(工作线程)直到委托执行完毕。使用它不会损害 UI 线程(并且它也不会让您的委托执行得更快或更可靠)。

我唯一一次使用

Invoke
遇到问题是当我在不同线程上有两个窗口时,它们都有自己的调度程序,并且它们每个都试图在另一个上
Invoke
- 因为
Invoke
会阻止调用线。但如果
Invoke
是从工作线程调用的,那么它唯一会阻塞的就是工作线程。因此,如果可能的话,永远不要阻塞你的 UI 线程,但绝对不要使用
Join

© www.soinside.com 2019 - 2024. All rights reserved.