在UI线程上等待任务而不会产生死锁

时间:2019-06-18 07:11:51

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

请紧记:我使用的是.NET 4.0,并且不能使用 异步/等待模式.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线程上创建死锁?其他答案建议使用异步/等待模式,但我无法使用它。

2 个答案:

答案 0 :(得分:0)

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

唯一的例外是,如果目标是不受支持的OS版本,例如Windows XP和Windows Server 2003,它们从未获得.NET 4.5支持。

无论如何,使用Microsoft.Bcl.Async 是一个可行的选择,因为您已经接受了更大的风险,例如在4.5上运行,而在目标4.0上运行,或在不受支持的OS上运行,甚至不提供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<T>。忘记这一点也会导致难以调试的问题。

使用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异步仅与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记录,但Microsoft最近更改了其博客引擎,旧的迁移博客文章的URL不再起作用。

由于既不支持.NET 4.0,也不支持Parallel Extras,因此链接没有固定。这些文章仍在a different URL

下提供

“不再受支持” 表示文档,库和知识也可能会丢失。 7年前从事某些工作的人们可能会记得当时使用的库和项目,但是7年太长了。

答案 1 :(得分:0)

尝试MSDN: Backgroundworker。如果您在表单的构造函数中完成了<p> <label>Forename:</label> <input class="w3-input w3-white" id="some1" type="text" name="some1" value="asd" maxLength="200"> </p> <button id="snbtn1" type="submit" class="w3-btn w3-green">send</button>,则可以通过执行backgroundworkerObject.CancelAsync();来取消任务。 这样可以在一个单独的线程中执行您的长时间操作,而不会导致UI死锁。