当线程使用分派器并且主线程正在等待线程完成时发生死锁

本文关键字:线程 在等待 完成时 死锁 分派 | 更新日期: 2023-09-27 18:08:45

谁能解释一下为什么这会造成死锁,以及如何解决它?

        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()。但是当我做调度员的时候。调用时,发生死锁,因为主线程正在等待次线程完成,因为它需要结果。

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

此外,从理论上讲,我不明白一件事:死锁只能发生在两个线程以不同的顺序访问两个共享资源时。但是在这种情况下实际的资源是什么呢?一个是GUI。但另一个是什么呢?我看不见。 而死锁通常是通过强加一个规则来解决的,即线程只能以精确的顺序锁定资源。我已经在别的地方做过了。但在这种情况下,既然我不知道实际的资源是什么,我该如何实施这条规则呢?

当线程使用分派器并且主线程正在等待线程完成时发生死锁

简短的回答:用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;
}

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

最好的解决方案是使用async:

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

Dispatcher正试图在UI消息循环中执行工作,但相同的循环目前被th.Join卡住,因此它们彼此等待,导致死锁。

如果你启动了一个Thread,然后马上又启动了一个Join,你肯定有代码的味道,应该重新考虑你在做什么。

如果你想在不阻塞UI的情况下完成事情,你可以简单地在InvokeAsyncawait

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

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

这会产生一个Join,它不会因为其他线程上的gui操作而阻塞。

这里的主要技巧是用一个空委托(无操作)阻塞Invoke,但具有低于队列中所有其他项目的优先级设置。这将迫使调度程序遍历整个队列。(默认优先级是DispatcherPriority.Normal = 9,所以我的DispatcherPriority.ContextIdle = 3远远低于。)

Join()调用使用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;
    }