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

Sni*_*tor 7 asp.net synchronizationcontext task-parallel-library webmethod async-await

问题

我有一个带有简单Web服务的ASP.NET 4.0 WebForms页面WebMethod.此方法用作异步/ TPL代码的同步包装器.我面临的问题是,内部Task有时会有一个null 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;
}
Run Code Online (Sandbox Code Playgroud)

存在的定义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);
    }
}
Run Code Online (Sandbox Code Playgroud)

分析

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

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

因为我不会"一直向下",所以这是一个"同步异步"的场景.根据Stephen Toub 撰写的这篇文章,标题为"如果我确实需要的内容"同步异步"?" 以下代码应该是一个安全的包装器:

Task.Run(() => holder.SampleTask()).Result
Run Code Online (Sandbox Code Playgroud)

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

Task.Factory.StartNew(
    () => holder.SampleTask().Result, 
    CancellationToken.None, 
    TaskCreationOptions.DenyChildAttach, 
    TaskScheduler.Default);
Run Code Online (Sandbox Code Playgroud)

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

我决定接近"原始"建议,并在.NET 4.5中实现:

Task.Run(() => holder.SampleTask()).Result
Run Code Online (Sandbox Code Playgroud)

这确实有效,因为它始终具有空同步上下文.哪,有点暗示Task.Run vs Task.Factory.StartNew文章有错吗?

务实的做法是升级到.NET 4.5,并使用Task.Run实现,但是这将会导致开发时间,我宁愿更紧迫的问题(如果可能)花.另外,我还想知道不同的情况TaskScheduler和TaskCreationOptions情况.

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

编辑

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

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

svi*_*ick 4

首先,在 ASP.NET 中使用同步异步通常没有多大意义。您正在承担创建和安排的开销Task,但您不会以任何方式从中受益。

\n\n

现在,回答你的问题:

\n\n
\n

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

\n
\n\n

嗯,ASP.NETThreadPool也使用同样的方法。但这与这里无关。相关的是,如果您Wait()(或调用Result,这是相同的)Task计划运行(但尚未启动),我TaskScheduler决定同步运行您的Task。这称为\xe2\x80\x9c任务内联\xe2\x80\x9d。

\n\n

这意味着您Task最终在 上运行SynchronizationContext,但实际上并未通过它进行调度。这意味着实际上不存在死锁的风险。

\n\n
\n

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

\n
\n\n

这与没有DenyChildAttach任何Task关系AttachedToParent。

\n\n
\n

我碰巧发现TaskCreationOptions.PreferFairness.NET 4.0 中的行为似乎如我所愿(所有执行都有一个空同步上下文),但不知道为什么会这样的情况下,我非常犹豫是否要使用它(它可能不会适用于所有场景)。

\n
\n\n

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

\n\n

编辑:

\n\n
\n

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

\n
\n\n

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

\n\n

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

\n