【问题标题】:Invoke inside Task.Run, how to solve deadlock?在Task.Run里面调用,怎么解决死锁?
【发布时间】:2017-10-23 15:58:02
【问题描述】:

我有一个静态方法,可以从任何地方调用。在执行过程中会遇到Invoke。显然,当从 UI 线程调用此方法时,它会死锁。

这是一个再现:

public static string Test(string text)
{
    return Task.Run(() =>
    {
        App.Current.Dispatcher.Invoke(() => { } );
        return text + text;
    }).Result;
}
void Button_Click(object sender, RoutedEventArgs e) => Test();

我已经阅读了多个问题,并且喜欢 @StephenCleary 的 10 个答案(甚至一些博客链接自这些),但我不明白如何实现以下目标:

  • 有一个静态方法,易于调用并从任何地方(例如 UI 事件处理程序、任务)获取结果;
  • 此方法应阻止调用方,之后调用方代码应继续在同一上下文中运行;
  • 此方法不应冻结 UI。

Test() 的行为最接近的类比是MessageBox.Show()

它可以实现吗?

P.S.:为了简短起见,我没有附上我的各种 async/await 尝试以及用于 UI 调用的尝试,但使用 DoEvents 的尝试看起来很糟糕。

【问题讨论】:

  • @PoweredByOrange 访问.Result 属性是阻塞调用。
  • 删除.Result 并设置返回类型Task<string> 然后你的调用代码可以await 结果。这将导致调用代码暂停,然后在获得结果后继续。这符合要求吗?
  • 如果在 UI 线程内调用,您的代码将阻塞 UI。如果要释放 UI 线程,您有两种选择:async 事件处理程序或BackgroundWorker
  • @FedericoDipuma BGW 已过时,根本不需要事件处理程序。使用 .NET 4.5+ 中的 IProgress< T> 类执行进度报告。或者只是分解 UI 中的方法和真正的异步部分并使用await 等待异步部分的执行
  • @Sinatr 你想做什么?您为什么要尝试从任务内部修改 UI?为什么不使用异步事件处理程序?

标签: c# wpf multithreading async-await


【解决方案1】:

你不能。

即使是这 3 个要求中的 2 个也无法同时实现 - “此方法应阻止调用者”与“此方法不应冻结 UI”相冲突。

您必须以某种方式使此方法异步(await,回调)或使其以小块可执行以仅在短时间内阻止 UI,例如使用计时器来安排每个步骤。

重申一下您已经知道的内容 - 您不能像在许多问题中讨论的那样阻止线程并同时将其回调 - await works but calling task.Result hangs/deadlocks

【讨论】:

  • 感谢您的回答,虽然只是说“不可能”并不是我今天早上希望找到的答案;)我提到MessageBox是有原因的,也许我的要求并不明确描述?我想从 UI 线程或某个 Task 调用 Test(),它应该 block 调用者,而不是冻结 UI。我将发布我糟糕的(但令人惊讶的工作)尝试作为答案,请看一下。
  • @Sinatr 编写对所有类型的消息都能正常工作并且同时不会导致意外重入问题的消息泵非常困难。我不会自己做,也不会推荐给任何人。至少仔细阅读msdn.microsoft.com/en-us/library/…blogs.msdn.microsoft.com/oldnewthing/20050428-00/?p=35753 中的警告。
  • 我知道DoEvents() 的问题,了解替代方案很有趣。目前,我正试图通过不必Invoke 来避免这种特殊情况。尽管问题很差(它被否决并且缺少重要细节 - Task 正在返回需要 Invoke 的东西),但我仍然不能接受“否”作为答案,抱歉;)我宁愿自己标记作为一个回答(虽然我不会,因为它的测试很差)。
【解决方案2】:

要实现MessageBox 所做的事情(但不创建窗口),可以执行以下操作:

public class Data
{
    public object Lock { get; } = new object();
    public bool IsFinished { get; set; }
}

public static bool Test(string text)
{
    var data = new Data();
    Task.Run(() =>
    {
        Thread.Sleep(1000); // simulate work
        App.Current.Dispatcher.Invoke(() => { });
        lock (data.Lock)
        {
            data.IsFinished = true;
            Monitor.Pulse(data.Lock); // wake up
        }
    });
    if (App.Current.Dispatcher.CheckAccess())
        while (!data.IsFinished)
            DoEvents();
    else
        lock (data.Lock)
            Monitor.Wait(data.Lock);
    return false;
}

static void DoEvents() // for wpf
{
    var frame = new DispatcherFrame();
    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new Func<object, object>(o =>
    {
        ((DispatcherFrame)o).Continue = false;
        return null;
    }), frame);
    Dispatcher.PushFrame(frame);
}

想法很简单:检查当前线程是否需要调用(UI 线程),然后运行DoEvents 循环或阻塞线程。

Test() 可以从 UI 线程或其他任务中调用。

它可以工作(虽然没有经过全面测试),但它很糟糕。我希望这将使我的要求明确,如果有更好的“不,你不能这样做”,我仍然需要回答我的问题;)

【讨论】:

  • 所有这些代码的意义何在?使用async/await,您什么都不需要。真正的问题是您仍然没有显示 任何东西 在需要报告的后台运行。您在此处编写的内容启动了一个任务,只是为了阻止它并尝试回调 UI 线程。好吧,如果您在任务中没有其他事情要做,请不要这样做并留在 UI 线程中。
  • @PanagiotisKanavos,谢谢,现在我看到了我想念的东西。注意返回值?它不会立即可用(我只是在这个 sn-p 中返回 false,甚至没有从任务中传递它)。代码现在阻塞调用者(UI 或非 UI 线程),直到结果可用,然后调用者应该收到结果并继续。能否以不同的方式实现?当您致电 SetResult 并且有人在等待它的 Task 时,这是 TaskCompletionSource 正在做的事情。但是在调用者不是async的情况下(调用者同步调用一些静态方法)。
猜你喜欢
  • 1970-01-01
  • 2018-07-29
  • 2016-02-24
  • 2019-07-11
  • 2023-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多