由TaskScheduler和SynchronizationContext混淆同步异步,无法控制同步上下文?

时间:2013-02-20 06:11:12

标签: asp.net task-parallel-library async-await webmethod synchronizationcontext

问题

我有一个带有简单Web服务WebMethod的ASP.NET 4.0 WebForms页面。此方法用作异步/ 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线程上结束。根据{{​​3}},“任务并行库和PLINQ的默认调度程序使用.NET Framework ThreadPool来排队和执行工作”。基于此,我希望SynchronizationContext内的SampleTask始终为空。

此外,我的理解是,如果SampleTask位于ASP.NET SynchronizationContext上,则.Result 中对MyWebMethod的调用可能死锁。

因为我不会“一直向下”,所以这是一个“同步异步”的场景。由Stephen Toub按this article标题为“如果我确实需要的话”同步“async”?,以下代码应该是一个安全的包装器:

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

根据this article,同样由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

工作,因为它始终具有空同步上下文。哪种情况表明this other article错了?

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

我巧合地发现.NET 4.0中的TaskCreationOptions.PreferFairness似乎表现得像我希望的那样(所有执行都有一个空的同步上下文),但不知道为什么这有效,我对使用它非常犹豫(它可能不适用于所有情况)。

修改

一些额外的信息......我用死锁的代码更新了我的示例代码,并包含一些调试输出以显示正在运行任务的线程。如果 前任务或继续任务输出指示与WebMethod相同的线程ID,则会发生死锁。

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

1 个答案:

答案 0 :(得分:4)

首先,在ASP.NET中使用sync-over-async通常没有多大意义。您需要承担创建和安排Task的费用,但不会以任何方式从中受益。

现在,问你的问题:

  

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

嗯,ASP.NET也使用相同的ThreadPool。但这并不重要。相关的是,Wait()(或致电Result,即Task计划运行的TaskScheduler(但不是{&1;}} #39; t start),Task我决定同步运行你的Task。这被称为“任务内联”。

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

  

感谢您使用.NET 4.0,我无法访问DenyChildAttach,我认为这是我的问题。

这与Task无关,AttachedToParent没有TaskCreationOptions.PreferFairness

  

我巧合地发现.NET 4.0中的PreferFairness似乎表现得像我所希望的那样(所有执行都有一个空同步上下文),但不知道为什么这是有效的,我对使用它非常犹豫(它可能不适用于所有情况)。

这是因为TaskThreadPool调度到全局队列(而不是每个Task线程所拥有的线程本地队列),看起来GetAsync() s从全局队列中不被内联。但我不会依赖这种行为,特别是因为它可能在未来发生变化。

修改

  

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

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

您需要做的是确保在没有设置同步上下文的情况下在线程上运行GetAsync()。我认为这样做的最佳方法是在致电{{1}}之前致电SynchronizationContext.SetSynchronizationContext(null)并在之后恢复。