2015-09-05 46 views
9

为什么以下异步递归会因StackOverflowException而失败?为什么在计数器变为零的最后一步恰好发生?尽管产生了意外的堆栈溢出

static async Task<int> TestAsync(int c) 
{ 
    if (c < 0) 
     return c; 

    Console.WriteLine(new { c, where = "before", Environment.CurrentManagedThreadId }); 

    await Task.Yield(); 

    Console.WriteLine(new { c, where = "after", Environment.CurrentManagedThreadId }); 

    return await TestAsync(c-1); 
} 

static void Main(string[] args) 
{ 
    Task.Run(() => TestAsync(5000)).GetAwaiter().GetResult(); 
} 

输出:

 
... 
{ c = 10, where = before, CurrentManagedThreadId = 4 } 
{ c = 10, where = after, CurrentManagedThreadId = 4 } 
{ c = 9, where = before, CurrentManagedThreadId = 4 } 
{ c = 9, where = after, CurrentManagedThreadId = 5 } 
{ c = 8, where = before, CurrentManagedThreadId = 5 } 
{ c = 8, where = after, CurrentManagedThreadId = 5 } 
{ c = 7, where = before, CurrentManagedThreadId = 5 } 
{ c = 7, where = after, CurrentManagedThreadId = 5 } 
{ c = 6, where = before, CurrentManagedThreadId = 5 } 
{ c = 6, where = after, CurrentManagedThreadId = 5 } 
{ c = 5, where = before, CurrentManagedThreadId = 5 } 
{ c = 5, where = after, CurrentManagedThreadId = 5 } 
{ c = 4, where = before, CurrentManagedThreadId = 5 } 
{ c = 4, where = after, CurrentManagedThreadId = 5 } 
{ c = 3, where = before, CurrentManagedThreadId = 5 } 
{ c = 3, where = after, CurrentManagedThreadId = 5 } 
{ c = 2, where = before, CurrentManagedThreadId = 5 } 
{ c = 2, where = after, CurrentManagedThreadId = 5 } 
{ c = 1, where = before, CurrentManagedThreadId = 5 } 
{ c = 1, where = after, CurrentManagedThreadId = 5 } 
{ c = 0, where = before, CurrentManagedThreadId = 5 } 
{ c = 0, where = after, CurrentManagedThreadId = 5 } 

Process is terminated due to StackOverflowException. 

我与.NET 4.6安装看到这个。该项目是一款面向.NET 4.5的控制台应用程序。

我明白,Task.Yield延续可能会在相同的线程调度由ThreadPool.QueueUserWorkItem(如上面#5),如果线程已被释放到池中 - await Task.Yield()之后,但QueueUserWorkItem回调以前一直实际上预定。

我不明白为什么和堆栈在哪里还在加深。即使它在同一个线程中调用,延续也不应该发生在同一个堆栈帧上。

我带了一步,并实施了Yield定制版本,确保延续不相同的线程上发生了:现在

public static class TaskExt 
{ 
    public static YieldAwaiter Yield() { return new YieldAwaiter(); } 

    public struct YieldAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion 
    { 
     public YieldAwaiter GetAwaiter() { return this; } 

     public bool IsCompleted { get { return false; } } 

     public void GetResult() { } 

     public void UnsafeOnCompleted(Action continuation) 
     { 
      using (var mre = new ManualResetEvent(initialState: false)) 
      { 
       ThreadPool.UnsafeQueueUserWorkItem(_ => 
       { 
        mre.Set(); 
        continuation(); 
       }, null); 

       mre.WaitOne(); 
      } 
     } 

     public void OnCompleted(Action continuation) 
     { 
      throw new NotImplementedException(); 
     } 
    } 
} 

,同时使用TaskExt.Yield代替Task.Yield,线程每个翻转时间,但堆栈溢出仍然存在:

 
... 
{ c = 10, where = before, CurrentManagedThreadId = 3 } 
{ c = 10, where = after, CurrentManagedThreadId = 4 } 
{ c = 9, where = before, CurrentManagedThreadId = 4 } 
{ c = 9, where = after, CurrentManagedThreadId = 5 } 
{ c = 8, where = before, CurrentManagedThreadId = 5 } 
{ c = 8, where = after, CurrentManagedThreadId = 3 } 
{ c = 7, where = before, CurrentManagedThreadId = 3 } 
{ c = 7, where = after, CurrentManagedThreadId = 4 } 
{ c = 6, where = before, CurrentManagedThreadId = 4 } 
{ c = 6, where = after, CurrentManagedThreadId = 5 } 
{ c = 5, where = before, CurrentManagedThreadId = 5 } 
{ c = 5, where = after, CurrentManagedThreadId = 4 } 
{ c = 4, where = before, CurrentManagedThreadId = 4 } 
{ c = 4, where = after, CurrentManagedThreadId = 3 } 
{ c = 3, where = before, CurrentManagedThreadId = 3 } 
{ c = 3, where = after, CurrentManagedThreadId = 5 } 
{ c = 2, where = before, CurrentManagedThreadId = 5 } 
{ c = 2, where = after, CurrentManagedThreadId = 3 } 
{ c = 1, where = before, CurrentManagedThreadId = 3 } 
{ c = 1, where = after, CurrentManagedThreadId = 5 } 
{ c = 0, where = before, CurrentManagedThreadId = 5 } 
{ c = 0, where = after, CurrentManagedThreadId = 3 } 

Process is terminated due to StackOverflowException. 
+2

很高兴看到您仍在使用匿名对象ToString技巧:) – usr

+0

@usr,自从我从您那里了解到它之后,它就是我的最爱之一:) – Noseratio

回答

8

TPL重入再次来袭:

请注意,在完成所有迭代后,堆栈溢出发生在函数的末尾。增加迭代次数不会改变这一点。将其降低一点就会消除堆栈溢出。

当完成方法TestAsync的异步状态机任务时发生堆栈溢出。它不会发生在“下降”。它会在退出并完成所有方法任务时发生。

让我们先减少计数到2000,以减少调试器的负载。然后,看看调用堆栈:

enter image description here

当然是非常重复和长。这是正确的线程来看待。坠机发生在:

 var t = await TestAsync(c - 1); 
     return t; 

当内任务完成t它导致外TestAsync其余的执行。这只是返回声明。退货完成外部TestAsync产生的任务。这又触发完成另一个t等等。

TPL将一些任务延续作为性能优化内联。这种行为已经引起了很多悲痛,就像Stack Overflow问题所证明的那样。 It has been requested to remove it.该问题相当老旧,至今尚未收到任何回复。这并不能激发我们最终摆脱TPL重入问题的希望。

当堆栈变得太深时,TPL会进行一些堆栈深度检查以关闭内存延续。这是因为我还不知道的原因。请注意,无处堆栈中有一个TaskCompletionSourceTaskAwaiter利用TPL中的内部功能来提高性能。也许这个优化的代码路径不会执行堆栈深度检查。也许这是一个错误。

我不认为调用Yield与该问题有任何关系,但最好将其放在此处以确保TestAsync的非同步完成。


让我们手动编写异步状态机:

static Task<int> TestAsync(int c) 
{ 
    var tcs = new TaskCompletionSource<int>(); 

    if (c < 0) 
     tcs.SetResult(0); 
    else 
    { 
     Task.Run(() => 
     { 
      var t = TestAsync(c - 1); 
      t.ContinueWith(_ => tcs.SetResult(0), TaskContinuationOptions.ExecuteSynchronously); 
     }); 
    } 

    return tcs.Task; 
} 

static void Main(string[] args) 
{ 
    Task.Run(() => TestAsync(2000).ContinueWith(_ => 
    { 
      //breakpoint here - look at the stack 
    }, TaskContinuationOptions.ExecuteSynchronously)).GetAwaiter().GetResult(); 
} 

感谢TaskContinuationOptions.ExecuteSynchronously我们也希望延续内联发生。确实如此,但它不会使栈溢出:

enter image description here

这是因为TPL可防止堆栈变得过深(如上所述)。在完成async方法任务时,此机制似乎不存在。

如果ExecuteSynchronously被删除,那么堆栈很浅并且不会发生内联。 await runs with ExecuteSynchronously enabled.

+0

一个很好的答案。事实上,这个问题的灵感来自客户服务员(我们称之为'AlwaysAsync'),我尝试解决您在此描述的完全相同的TPL问题。我在TestAsync中使用它,但不在其返回行。所以我刚刚把返回行更改为'返回等待TestAsync(c-1).AlwaysAsync()'并且问题消失了:) – Noseratio

+0

另一种消除堆栈跳转的方法是使用'Task.Run': 'async任务 TestAsync(int c){if(c <0)return c;返回等待Task.Run(()=> TestAsync(c-1)); }'。 – Noseratio

+0

@Noseratio是否在实践中起作用?我认为完成可能会一直内联。 Task.Run里面有特殊的perf优化解包代码。也许这里的堆栈溢出避免机制已经到位并且正在工作。 – usr