7

问题

我有一个带有简单 web-service 的 ASP.NET 4.0 WebForms 页面WebMethod。此方法用作异步/TPL 代码的同步包装器。我面临的问题是内部Task有时有一个空值SynchronizationContext(我的偏好),但有时有一个同步上下文System.Web.LegacyAspNetSynchronizationContext。在我提供的示例中,这并不会真正导致问题,但在我的实际开发场景中可能会导致死锁。

对服务的第一次调用似乎总是使用空同步上下文运行,接下来的几个也可能如此。但是一些快速请求,它开始弹出到 ASP.NET 同步上下文中。

编码

[WebMethod]
public static string MyWebMethod(string name)
{
    var rnd = new Random();
    int eventId = rnd.Next();
    TaskHolder holder = new TaskHolder(eventId);

    System.Diagnostics.Debug.WriteLine("Event Id: {0}. Web method thread Id: {1}",
        eventId,
        Thread.CurrentThread.ManagedThreadId);

    var taskResult = Task.Factory.StartNew(
        function: () => holder.SampleTask().Result,
        creationOptions: TaskCreationOptions.None,
        cancellationToken: System.Threading.CancellationToken.None,
        scheduler: TaskScheduler.Default)
        .Result;

    return "Hello " + name + ", result is " + taskResult;
}

存在的定义TaskHolder

public class TaskHolder
{
    private int _eventId;
    private ProgressMessageHandler _prg;
    private HttpClient _client;

    public TaskHolder(int eventId)
    {
        _eventId = eventId;
        _prg = new ProgressMessageHandler();
        _client = HttpClientFactory.Create(_prg);
    }

    public Task<string> SampleTask()
    {
        System.Diagnostics.Debug.WriteLine("Event Id: {0}. Pre-task thread Id: {1}",
            _eventId,
            Thread.CurrentThread.ManagedThreadId);

        return _client.GetAsync("http://www.google.com")
            .ContinueWith((t) =>
                {
                    System.Diagnostics.Debug.WriteLine("Event Id: {0}. Continuation-task thread Id: {1}",
                        _eventId,
                        Thread.CurrentThread.ManagedThreadId);

                    t.Wait();

                    return string.Format("Length is: {0}", t.Result.Content.Headers.ContentLength.HasValue ? t.Result.Content.Headers.ContentLength.Value.ToString() : "unknown");
                }, scheduler: TaskScheduler.Default);
    }
}

分析

我的理解TaskScheduler.Default是它是ThreadPool调度程序。换句话说,线程不会在 ASP.NET 线程上结束。根据这篇文章“任务并行库和 PLINQ 的默认调度程序使用 .NET Framework 线程池来排队和执行工作”。基于此,我希望SynchronizationContext内部SampleTask始终为空。

此外,我的理解是,如果SampleTask要在 ASP.NET 上,对inSynchronizationContext的调用可能会死锁。.ResultMyWebMethod

因为我不会“一直异步”,所以这是一个“同步异步”场景。根据Stephen Toub 的这篇文章,在标题为“如果我真的需要“同步而不是异步”怎么办?以下代码应该是一个安全的包装器:

Task.Run(() => holder.SampleTask()).Result

根据同样由 Stephen Toub 撰写的另一篇文章,上述内容在功能上应等同于:

Task.Factory.StartNew(
    () => holder.SampleTask().Result, 
    CancellationToken.None, 
    TaskCreationOptions.DenyChildAttach, 
    TaskScheduler.Default);

由于在 .NET 4.0 中,我无法访问TaskCreationOptions.DenyChildAttach,我认为这是我的问题。但是我在 .NET 4.5 中运行了相同的示例并切换到TaskCreationOptions.DenyChildAttach它,它的行为相同(有时会抓取 ASP.NET 同步上下文)。

然后我决定更接近“原始”建议,并在 .NET 4.5 中实现:

Task.Run(() => holder.SampleTask()).Result

确实有效,因为它总是有一个空同步上下文。哪一种暗示Task.Run 与 Task.Factory.StartNew 文章有问题?

务实的方法是升级到 .NET 4.5 并使用Task.Run实现,但这会涉及我宁愿花在更紧迫问题上的开发时间(如果可能的话)。另外,我仍然想弄清楚不同TaskSchedulerTaskCreationOptions场景发生了什么。

我巧合地发现,TaskCreationOptions.PreferFairness在 .NET 4.0 中似乎表现得如我所愿(所有执行都有一个空同步上下文),但不知道为什么会这样,我很犹豫要不要使用它(它可能无法正常工作)情景)。

编辑

一些额外的信息...我已经用一个死锁的代码更新了我的示例代码,包含一些调试输出以显示任务正在运行的线程。如果 pre-task 或 continuation-task 输出指示与 WebMethod 相同的线程 id,则会发生死锁

奇怪的是,如果我不使用 ProgressMessageHandler,我似乎无法复制死锁。我的印象是这无关紧要,无论下游代码如何,我都应该能够使用正确的Task.Factory.StartNew或方法在同步上下文中安全地“包装”异步Task.Run方法。但这似乎并非如此?

4

1 回答 1

4

首先,在 ASP.NET 中使用 sync-over-async 通常没有多大意义。您会产生创建和调度Tasks 的开销,但您不会以任何方式从中受益。

现在,对于你的问题:

我的理解TaskScheduler.Default是它是ThreadPool调度程序。换句话说,线程不会在 ASP.NET 线程上结束。

好吧,ASP.NETThreadPool也使用相同的方法。但这在这里并不重要。相关的是,如果您Wait()(或调用Result,相同)在Task计划运行(但尚未开始)上,TaskScheduler我决定只Task同步运行您的。这被称为“任务内联”。

这意味着您Task最终会在 上运行SynchronizationContext,但实际上并没有通过它安排。这意味着实际上没有死锁的风险。

由于在 .NET 4.0 中,我无法访问TaskCreationOptions.DenyChildAttach,我认为这是我的问题。

这无关DenyChildAttach,没有Tasks那会AttachedToParent

我巧合地发现,TaskCreationOptions.PreferFairness在 .NET 4.0 中似乎表现得如我所愿(所有执行都有一个空同步上下文),但不知道为什么会这样,我很犹豫要不要使用它(它可能无法正常工作)情景)。

这是因为将 sPreferFairness调度Task到全局队列(而不是每个ThreadPool线程具有的线程本地队列),并且似乎Task来自全局队列的 s 不会被内联。但我不会依赖这种行为,特别是因为它将来会改变。

编辑:

奇怪的是,如果我不使用 ProgressMessageHandler,我似乎无法复制死锁。

没什么好奇怪的,这正是你的问题。ProgressMessageHandler 报告当前同步上下文的进度。由于任务内联,这就是 ASP.NET 上下文,您通过同步等待来阻止它。

您需要做的是确保GetAsync()在没有设置同步上下文的线程上运行。我认为最好的方法是在调用SynchronizationContext.SetSynchronizationContext(null)之前调用GetAsync()并在之后恢复它。

于 2013-02-20T11:00:42.297 回答