【问题标题】:Wait for Task on UI Thread without producing a deadlock等待 UI 线程上的任务而不产生死锁
【发布时间】:2019-06-18 07:11:51
【问题描述】:

请记住:我使用的是 .NET 4.0,并且不能使用 async/await 模式.ConfigureAwait

我目前正在尝试在执行长时间运行的操作时保持 UI 响应,主要是为了能够在需要时取消操作。

因此,我使用Task.Factory.StartNew 在 UI 线程上启动新任务,并使用Wait 等待其完成。

在我继续之前操作完成很重要,这就是我使用Wait 等待它完成的原因。 但是,这会在 UI 线程上造成死锁。

完整代码:

// currently on the UI thread
Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(x =>
{
    // simple output that I'm done
}).Wait(); // -> deadlock. can't remove it, otherwise the app would continue

调用该代码看起来像一个普通的函数调用

private void Run(){
    DoStuff();
    DoMoreStuff(); // it's important that DoStuff has finished, that's why removing .Wait won't work
}

private void DoStuff()
{
    Task.Factory.StartNew(() => LongerOperation())
    .ContinueWith(x =>
    {
        // simple output that I'm done
    }).Wait();
}

如何等待任务完成而不在 UI 线程上创建死锁?其他答案建议使用 async/await 模式,但我无法使用。

【问题讨论】:

  • “请记住:我在 .NET 4.0 上,不能使用 async/await 模式,也不能使用 .ConfigureAwait。” 有没有办法使用它? @DmitryBychenko
  • 不是死锁。它是阻塞的,这使得所有异步变得毫无意义。
  • 您是否确定您使用的是 .NET 4.0?不只是 .NET 4.0 已经停止支持好几年了。所有 4.x 版本都是以前版本的二进制替换,这意味着,除非您以 Windows XP(几年也不受支持)为目标,否则无论如何您都在 .NET 4.5 上运行 - 任何安装或 Windows 更新在最后一个 中安装了 4.5 7 年将用新的运行时替换 4.0
  • @delayedExtension 如果您坚持针对过时的运行时,Microsoft.Bcl.Async 是可以接受的。您已经接受了更高的风险,包括安全风险。 .NET 4.0 不支持 TLS1.2,这是当今的最低要求。在任何情况下,除非您以 Windows XP(或锁定的嵌入式计算机)为目标,否则您无法以 4.0 为目标,即使您愿意。
  • @delayedExtension 在任何情况下,人们在不是 BGW 或 BeginInvoke 时更新 UI 的方式。使用 ContinueWith 和 TaskSchedulerOption 在 UI 上运行

标签: c# winforms c#-4.0 task deadlock


【解决方案1】:

对于初学者,您可能没有在 .NET 4.0 上工作,因为 .NET 4.x 运行时是二进制替代品。安装 .NET 4.5+ 应用程序或 Windows 更新补丁意味着所有应用程序现在都可以在 .NET 4.5 及更高版本上运行。您的开发机器至少可以在 .NET 4.5+ 上运行,这意味着您已经在与您打算定位的运行时不同的运行时上进行开发。

唯一的例外是,如果您面向不受支持的操作系统版本,例如 Windows XP 和 Windows Server 2003,它们从未获得 .NET 4.5 支持。

无论如何,使用Microsoft.Bcl.Async 一个可行的选择,因为您已经接受了更大的风险,例如在 4.5 上运行而以 4.0 为目标,或者在不受支持的操作系统上运行甚至不支持 TLS1.2,这是当今大多数服务的最低要求,包括 Google、AWS、Azure、银行、支付网关、航空公司等。

另一个选项是将ContinueWithTaskScheduler 参数一起使用,该参数指定在何处运行延续。 TaskScheduler.FromCurrentSynchronizationContext() 指定延续将在原始同步上下文上运行。在 Winforms 或 WPF 应用程序中,这就是 UI 线程。

那时候我们经常写这样的代码:

Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(t =>
{
    textBox.Text=String.Format("The result is {0}",t.Result);
},TaskScheduler.FromCurrentSynchronizationContext());

处理异常和链接多个异步操作需要额外的工作。如果任务失败,t.Result 会抛出异常,所以你需要在尝试使用它的值之前检查Task.IsFaultedTask.IsCancelled

没有办法像使用 .NET 4.5 和 async/await 那样使用简单的 return 来缩短延续链。您可以检查 IsFaulted 标志并避免更新 UI,但您不能阻止 next 继续沿链执行。

Task.Factory.StartNew(() => LongerOperation())
    .ContinueWith(t=>
    {
        if (!t.IsFaulted)
        {
            return anotherLongOperation(t.Result);
        }
        else
        {
            //?? What do we do here?
            //Pass the buck further down.
            return Task.FromException(t.Exception);
        }
    })         
    .ContinueWith(t =>
    {
        //We probably need `Unwrap()` here. Can't remember
        var result=t.Unwrap().Result;
        textBox.Text=String.Format("The result is {0}",result);
    },TaskScheduler.FromCurrentSynchronizationContext());

要在失败的情况下停止执行,您必须使用TaskContinuationOptions 参数并传递例如NotOnFaulted

Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(t =>
    {
        textBox.Text=String.Format("The result is {0}",t.Result);
    },
    CancellationToken.None,
    TaskContinuationOptions.NotOnFaulted,
    TaskScheduler.FromCurrentSynchronizationContext());

如果由于某些业务规则而想结束执行,您会怎么做?您必须返回 一些东西,所有后续步骤都需要转发到链的末尾。

最后,使用Microsoft.Bcl.Async 可以得到更简单的代码

Task.Factory.StartNew(() => LongerOperation())
.ContinueWith(t=>
{
    if (!t.IsFaulted)
    {
        var num=anotherLongOperation(t.Result);
        if (num<0)
        {
            //Now what?
            //Let's return a "magic" value
            return Task.FromResult(null);
        }
    }
    else
    {
        //?? What do we do here?
        //Pass the buck further down.
        return Task.FromException(t.Exception);
    }
})         
.ContinueWith(t =>
{
    //This may need t.Result.Result
    if (!t.IsFaulted && t.Result!=null)
    {
        textBox.Text=String.Format("The result is {0}",t.Result);
    }
},TaskScheduler.FromCurrentSynchronizationContext());

我不确定后续是否需要Unwrap()。他们可能会这样做,因为第二步返回Task&lt;T&gt;。忘记这一点也会导致难以调试的问题。

使用Microsoft.Bcl.Async 可以得到更干净、更简单的代码,而且更容易正确:

public async Task DoRun()
{
    try
    {
        var x=await longOperation();
        var num=await anotherLongOperation(x);
        if(num<0)
        {
            return;
        }
        textBox.Text=String.Format("The result is {0}",num);
    }
    catch(Exception exc)
   {
      //Do something about it
   }
}

更新

正如 Stephen Cleary 所指出的,BLC Async 仅适用于 Visual Studio 2012。它也不再受支持,并且可能仅作为 MSDN 订阅者的下载提供。

早在 2010 年,Parallel 团队就发布了一些扩展和示例,以简化并行和异步处理,Parallel Extensions Extras。该库包含像 Then 这样的外部组件,它使链接和错误处理变得更加容易。

其中大部分功能自 4.5 起已包含在 .NET 本身中,因此示例和库自 2011 年以来未更新。任何声称是 Parallel Extras 的 NuGet 包或 Github 存储库只需克隆并重新打包此下载。

该库曾经a series of articles by Stephen Toub 记录,但微软最近更改了其博客引擎,旧的迁移博客文章的 URL 不再有效。

由于既不支持 .NET 4.0 也不支持 Parallel Extras,因此未修复链接。这些文章仍然可以在a different URL 下获得

“不再支持” 表示文档、库和知识也可能丢失。有些 7 年前做任务的人可能还记得当时使用的库和项目,但 7 年太长了。

【讨论】:

  • 重要提示:总是TaskScheduler 传递给StartNewContinueWith。这避免了problems with ambient task schedulers
  • 我认为这些天不可能让Microsoft.Bcl.Async 工作。您需要对旧版本的 VS 进行特定更新,我相信它不再可供下载。当我丢失了用于 AsyncEx 的构建机器时,我花了几个月的时间尝试重新创建可以将 Microsoft.Bcl.Async 与 Silverlight、WinPhone 和所有这些旧平台一起使用的东西。最终我放弃了,不再支持他们。
  • @StephenCleary 和那个时代的 Pfx 团队博客文章可能在 Microsoft 的博客引擎迁移中丢失或损坏,以及记录如何使用 Parallel Extension Extras 的文章。
【解决方案2】:

试试MSDN: Backgroundworker。如果您在表单的构造函数中完成了backgroundWorkerObject.WorkerSupportsCancellation = true;,您可以通过执行backgroundworkerObject.CancelAsync(); 来取消您的任务。 这会在单独的线程中执行您的长时间操作,而不会导致您的 UI 出现死锁。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-28
    • 1970-01-01
    • 1970-01-01
    • 2016-02-10
    • 1970-01-01
    相关资源
    最近更新 更多