异步编程

异步因果链跟踪

Andrew Stasyuk

下载代码示例

 

5 C#、 Visual Basic.NET 11、 Microsoft.NET Framework 4.5 和 Windows 存储区为.NET 应用程序的到来,异步编程经验已被大大简化。 新的异步等待关键字 (异步和等待在 Visual Basic 中的) 允许开发人员保持它们被用来编写同步代码时的同一抽象。

大量的精力投入 Visual Studio 2012 以改善异步调试并行堆栈、 并行任务、 并行手表和并发可视化工具等工具。 然而,正在与调试经验的同步代码,我们在不很有尚未。

打破了抽象和揭示背后等待异步/大三巴牌坊内部水管更突出的问题之一是缺乏在调试器中调用堆栈信息。 在本文中,我提供手段,弥合这种差距和改善您的.NET 4.5 或 Windows 存储应用程序异步的调试体验。

让我们首先解决基本的术语。

调用堆栈的定义

MSDN 文档 (bit.ly/Tukvkm) 用来定义调用堆栈作为"从程序的开头通往目前在运行时执行的语句的方法调用的系列"。这种观念是完全有效的单线程、 同步编程模型,但现在,并行性和异步获得势头,更精确的分类是必要的。

本文中,是重要的是要区分从返回堆栈的因果关系链。 在同步模式中,这两个名词大部分都相同 (稍后我会提及例外情况下)。 在异步代码中,上述定义描述的因果关系链。

另一方面,当前正在执行,当完成后,该语句将导致一系列继续其执行的方法。 这一系列构成返回堆栈。 或者,为读者熟悉继续传递样式 (Eric Lippert 有很棒的一系列关于这一主题,起价 bit.ly/d9V0Dc),返回堆栈可能定义为一系列延续注册要执行,应当前执行的方法完成。

简而言之,因果关系链回答,"我怎么在这里?"返回堆栈时的答案,"我到哪里下?"例如,如果你有一个死锁在您的应用程序,您可能能够找出是什么导致它从前者,而后者会让你知道后果是什么。 请注意虽然因果关系链始终跟踪回程序入口点,返回堆栈切断在异步操作的结果都没有遵守 (为例如,异步 void 方法或通过 ThreadPool.QueueUserWorkItem 计划工时) 点。

此外,还有一个被保留用于诊断 ; 同步调用堆栈的副本的堆栈跟踪的概念 我将交替使用这两个名词。

请注意有几个心照不宣的假设,在前面的定义:

  • "方法调用"所指的第一定义中一般暗示"的方法,仍未完成,"而承受同步编程模型中的物理意义的"是在堆栈上"。 然而,虽然我们通常不感兴趣已经返回的方法,它总是不可能将它们区分在异步调试过程中。 在这种情况下,没有物理概念的"堆栈上的是"和所有延续都相等的因果关系链的有效元素。
  • 即使在同步代码中,因果关系链和返回堆栈不总是相同。 一个特别的案件时一种方法可能会出现在一个,但从其他,失踪是尾调用。 虽然讲得不直接通 C# 和 Visual Basic.NET 中可能编码在中间语言 (IL) ("尾巴"。 前缀) 或者 (尤其是在 64 位进程) 中时 (JIT) 编译器生成的。
  • 最后但并非最不重要,因果关系链和返回堆栈可以是非线性。 他们就是最一般的情况下,他们只是图有接收器 (因果图) 或源 (返回图) 作为当前语句。 在异步代码中的非线性是由于叉 (源自一个并行异步操作) 和联接 (继续计划运行一组并行异步操作完成时)。 本文中,并且由于平台限制 (稍后解释),我认为只有线性因果关系链和返回堆栈,哪些相应的图的子集。

幸运的是,如果不同步引入一个程序通过使用异步和等待关键字与没有叉或联接,并且正在等待所有异步方法,因果关系链仍然相同是返回堆栈,同步代码中一样。 在这种情况下,他们俩都是同样有益于你控制流中的定位。

另一方面,因果关系链相等很少返回堆栈在程序中显式地雇用计划延续,一个显著的例子是任务并行库 (TPL) 数据流。 这是由于数据从源块流到目标块,永远不会返回到前者的性质。

现有的工具

请考虑一个简单的例子:

static void Main()
{
  OperationAsync().Wait();
}
async static Task OperationAsync()
{
  await Task.Delay(1000);
  Console.WriteLine("Where is my call stack?");
}

由外推的开发者已经习惯了在同步调试中的抽象,他们希望看到下面的因果关系链/返回堆栈执行暂停在 Console.WriteLine 方法时:

ConsoleSample.exe!ConsoleSample.Program.OperationAsync() Line 19
ConsoleSample.exe!ConsoleSample.Program.Main() Line 13

但是,如果您尝试这样做,你就会发现在调用堆栈窗口中的 Main 方法中丢失,直接在前面加 [恢复异步方法] 的 OperationAsync 方法中的堆栈跟踪启动时。 并行堆栈有两种方法 ; 然而,它不会显示主调用 OperationAsync。 并行任务并不能帮助,显示"没有任务以显示"。

注:此时,调试器是意识到 Main 方法的调用堆栈的一部分 — — 你可能已经注意到,通过对 OperationAsync 的调用背后的灰色背景。 CLR 和 Windows 运行库 (WinRT) 有知道在哪里,继续执行后返回最顶层的堆栈帧 ; 因此,它们实际上存储返回堆栈。 在这篇文章,不过,我将只深入跟踪,作为另一篇文章的主题离开返回堆栈的因果关系。

保留的因果关系链

事实上,因果关系链永远不会存储由运行库中。 即使您看到调试时同步代码是从本质上讲的调用堆栈返回堆栈 — — 正如刚才所说的话,他们是 CLR 和 Windows 运行时必须知道哪些方法执行后返回最顶层的框架。 运行时不需要知道是什么导致了要执行的特定方法。

若要能够查看期间活的因果关系链和验尸调试,必须明确保留他们一路走来。 据推测,这将需要存储位置安排延续每一点 (同步) 的堆栈跟踪信息和将此数据还原时继续开始执行。 这些堆栈的跟踪段可以然后缝合在一起形成的因果关系链。

我们更感兴趣的因果关系信息传输跨越等待构造,因为这是抽象的相似性与同步代码出现失灵。 让我们看看如何以及何时可以捕获此数据。

正如斯蒂芬 · Toub 指出 (bit.ly/yF8eGu),但条件是 FooAsync 返回一项任务,下面的代码:

await FooAsync();
RestOfMethod();

这大致相当于编译器进行转换:

var t = FooAsync();
var currentContext = SynchronizationContext.Current;
t.ContinueWith(delegate
{
  if (currentContext == null)
    RestOfMethod();
  else
    currentContext.Post(delegate { RestOfMethod(); }, null);
}, TaskScheduler.Current);

从扩大的代码来看,似乎有至少两个扩展点,可能会允许捕获因果关系信息:TaskScheduler 和 SynchronizationContext。 事实上,两者都提供类似成对的虚拟方法,它应该有可能在正确的时刻捕获调用堆栈段:QueueTask/TryDequeue TaskScheduler 和邮政/OperationStarted SynchronizationContext 上。

不幸的是,显式等等计划通过第三方物流的 API,如 Task.Run、 Task.ContinueWith、 TaskFactory.StartNew 的委托时,只可以替代默认 TaskScheduler。 这意味着每当外面正在运行的任务安排延续,默认的 TaskScheduler 将生效。 因此,TaskScheduler-­的的做法将无法获取必要的信息。

至于 SynchronizationContext,虽然它是可以重写默认实例的当前线程的此类通过调用 SynchronizationContext.SetSynchronizationContext 方法,这已在应用程序中的每个线程的工作要做。 因此,必须要能够控制线程生存期,这是不可行的如果你不打算重新实现线程池。 此外,Windows 窗体、 Windows 的演示文稿基础 (WPF) 和 ASP.NET 提供的 SynchronizationContext SynchronizationContext.Default,其中安排到线程池工作除了自己的实现。 因此,您的实现必须根据起源的线程,它正在以不同的方式表现。

此外请注意,当等待 awaitable 自定义时,它是完全由执行是否能够使用 SynchronizationContext 来计划继续进行。

幸运的是,有两个扩展点适合我们的场景:订阅第三方物流事件而无需修改现有的基本代码,或显式选择通过略有修改每个等待应用程序中的表达式。 第一种方法仅适用于.NET 的桌面应用程序,而第二个可以容纳 Windows 存储区的应用程序。 我会详细说明在以下各节中的两个。

介绍 EventSource

.NET Framework 支持事件跟踪 Windows (ETW),定义了几乎每个方面的运行时事件提供程序 (bit.ly/VDfrtP)。 特别是,第三方物流将触发事件使您可以跟踪任务的生存期。 虽然不是所有这些事件都有记录,您可以获取它们的定义自己的深入研究与工具 (如 ILSpy 或反射器 mscorlib.dll 或偷窥框架参考源 (bit.ly/HRU3) 和搜索的 TplEtwProvider 类。 当然,通常反射免责声明适用:API 不记录,则不能保证将在下一版本中保留根据经验观察到的行为。

TplEtwProvider 从 System.Diagnostics.Tracing.EventSource,这在.NET 框架 4.5 中引入和现在是推荐的方法,以激发 ETW 事件在应用程序中的继承 (以前,您必须处理的手册 ETW 清单生成)。 此外,EventSource 通过订阅他们通过 EventListener,.NET 框架 4.5 (更多关于这瞬间) 中的新功能还允许消费的过程中,发生的事件。

可以按名称或 GUID 标识的事件提供程序。 每个特定事件类型是反过来确定的事件 ID 和可选的关键字来区分其他无关 (TplEtwProvider 不使用关键字) 此提供程序激发的事件的类型。 有可选的任务和操作码参数,您可能会发现有用的筛选,但我会单靠事件 id。 每个事件还定义详细的级别。

第三方物流的事件有各种各样的除了因果关系链,例如跟踪的任务飞行、 遥测等用途。 虽然他们别为自定义的 awaitables,火。

介绍 EventListener

在.NET Framework 4 中,以捕获 ETW 事件,您不得不将运行进程外 ETW 侦听程序,如 Windows 性能记录器或万斯臣 PerfView,以及与您在调试器中观察到的状态然后关联捕获的数据。 这构成额外的问题,因为数据存储在进程的内存空间外和崩溃转储并不包括它,使此解决方案验尸调试不太适合。 例如,如果您依赖于 Windows 错误报告功能,以提供转储,你不会得到任何 ETW 跟踪并因此因果关系信息将会丢失。

但是,.NET 框架 4.5 中开始,也可以订阅到第三方物流活动 (和其他由 EventSource 继承者激发的事件),请通过 System.Diagnostics.Tracing.EventListener (bit.ly/XJelwF)。 这允许捕获和保存的进程的内存空间中的堆栈跟踪段。 因此,一个小型的堆转储应该足以提取因果关系的信息。 在本文中,我将只详细信息基于 EventListener 的订阅。

值得一提的一个进程外侦听器的优点您总是可以获得的调用堆栈的堆栈 ETW 事件 (依赖于现有的工具或做单调乏味的堆栈遍历和跟踪自己的模块地址) 听。 订阅使用 EventListener 事件,当你进不去的调用堆栈信息 Windows 存储区的应用程序,因为 StackTrace API 禁止。 (适用于 Windows 存储应用程序的方法被介绍以后。

要订阅事件,您必须从事件继承­侦听器,写 OnEventSourceCreated 方法,并确保您的侦听器实例获取创建在您的程序的每个 AppDomain (订阅是每个应用程序域中)。 EventListener 实例化之后,将调用此方法来通知正在创建的事件源的侦听器。 它还将提供创建侦听器之前存在的所有事件源的通知。 过滤事件源可以按名称或 GUID (performance-wise,比较的 Guid 是一个好主意) 后, 调用 EnableEvents 订阅源的侦听器:

private static readonly Guid tplGuid =
  new Guid("2e5dba47-a3d2-4d16-8ee0-6671ffdcd7b5");
protected override void OnEventSourceCreated(EventSource eventSource)
{
  if (eventSource.Guid == tplGuid)
    EnableEvents(eventSource, EventLevel.LogAlways);
}

若要处理的事件,您需要实现抽象方法 OnEventWritten。 保存和恢复堆栈跟踪部分,您需要捕获调用堆栈右前安排了一个异步操作,以及当它开始执行,然后,与之关联的存储的堆栈跟踪段。 要关联这两个事件,您可以使用 TaskID 参数。 装箱到只读对象集合参数传递给事件源中的相应事件触发方法并将其作为有效载荷的 EventWrittenEventArgs 属性传递。

有趣的是,有特别快速路径为 EventSource 事件所消耗 ETW (而不是通过 EventListener),作为在拳击不会发生他们的论点。 这确实提供了一个性能的改进,但大多是瞄准了由于跨进程机械。

在 OnEventWritten 方法中,您需要区分事件源 (在您订阅多个的情况),并确定这一事件本身。 堆栈跟踪将捕获 (存储) 时触发 TaskScheduled 或 TaskWaitBegin 的事件,和关联一个新开工的异步操作 (还原) 在 TaskWaitEnd 中。 您还需要在相关标识符作为 taskId 中传递。 图 1 显示如何将处理事件的轮廓。

图 1 的 OnEventWritten Method 中的第三方物流事件的处理

protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
  if (eventData.EventSource.Guid == tplGuid)
  {
    int taskId;
    switch (eventData.EventId)
    {
      case 7: // Task scheduled
        taskId = (int)eventData.Payload[2];
        stackStorage.StoreStack(taskId);
        break;
      case 10: // Task wait begin
        taskId = (int)eventData.Payload[2];
        bool waitBehaviorIsSynchronous =
          (int)eventData.Payload[3] == 1;
        if (!waitBehaviorIsSynchronous)
          stackStorage.StoreStack(taskId);
        break;
      case 11: // Task wait end
        taskId = (int)eventData.Payload[2];
        stackStorage.RestoreStack(taskId);
        break;
    }
  }
}

注:在代码中的显式值 ("幻数") 是不好的编程做法和在这里仅用于简洁。 随附的示例代码项目有他们方便的结构常数和枚举,以避免重复和打字错误的风险。

请注意在 TaskWaitBegin,我检查为 TaskWaitBehavior 正在同步的这种情况发生时正在等待的任务同步执行或已经完成。 在这种情况下,同步调用堆栈是仍在的地方,因此它不需要显式存储。

异步本地存储

无论您选择保留调用堆栈段的数据结构需要以下质量:应保留存储的值 (因果关系链),为每一个异步操作,下列控制流跨一路上等待边界和延续,同时牢记,延续可能在不同的线程上执行。

这表明一个类似于线程的本地变量,将维护有关当前的异步操作 (链的延续),其值,而不是特定的线程。 大致可以命名"异步本地存储"。

CLR 已称为 ExecutionContext,已在一个线程上捕获和还原另一方面 (其中继续获取执行),因此正在传递和控制流的数据结构。 这是本质上是一个容器,存储可能需要继续执行完全相同的环境,在那里它们被中断其他上下文 (SynchronizationContext、 CallContext 等等)。 斯蒂芬 Toub 已详情,请致电 bit.ly/M0amHk。 最重要的是,您可以在其中似乎适合上述目的 (通过调用静态方法中 LogicalSetData 和 LogicalGetData),调用上下文中存储任意数据。

铭记这 CallContext (其实,内部有两个:LogicalCallContext 和 IllogicalCallContext) 是一个沉重的对象,旨在跨越远程处理边界流。 没有自定义数据存储时,运行时不会初始化上下文,盘与控制流保持它们的成本。 尽快调用 CallContext.LogicalSetData 方法时,可变的 ExecutionContext 和几个哈希表已创建并传递或从此克隆。

不幸的是,ExecutionContext (以及所有其成分) 与之前所述的第三方物流事件消防捕获并不久恢复。 因此,保存在 CallContext 之间的任何自定义数据将被丢弃,ExecutionContext 还原后,这将使它不适合我们特定的目的。

此外,调用类中不可用 Windows 存储区为.NET 应用程序子集,所以这种情况下需要另一种方法。

生成异步本地存储,将解决这些问题的一种方法是同步的部分代码执行时,使用线程本地存储 (TLS) 中维持的价值。 然后,会触发 TaskWaitStart 事件,当共享的 (非 TLS) 字典,由 TaskID 键控中存储值。 时对应的事件,TaskWaitEnd,触发时,从字典中移除的保留的值并将其保存回 TLS,可能在不同的线程上。

您可能知道,线程返回到线程池并获取执行的新工作后,仍保留在 TLS 中存储的值。 所以,有些时候,价值已从 TLS 中删除 (否则,一些其他异步操作线程上执行此稍后可能访问犹如它是自己以前操作所存储的值)。 因为嵌套的情况下,不能执行此 TaskWaitBegin 事件处理程序中操作等待,TaskWaitBegin 和 TaskWaitEnd 事件发生多次,等待,每一次和一个存储的值,可能需要在之间,如下面的代码段:

async Task OuterAsync()
{
  await InnerAsync();
}
async Task InnerAsync()
{
  await Task.Delay(1000);
}

相反,它是安全考虑在 TLS 中值是有资格被清除当前的异步操作不再在一个线程上执行时。 因为 CLR 没有在-­将通知的循环再用回线程池中的线程的进程事件 (有 ETW 之一 — —bit.ly/ZfAWrb),为此目的,我将使用 ThreadPoolDequeueWork FrameworkEventSource (也无证),由激发的线程池线程上开始新的操作时发生。 这便排除了非汇集的线程,要手动清理 TLS 中的,例如当 UI 线程返回到消息循环。

这一概念与捕获堆栈段和串联的工作实现,请参阅附带的源代码代码下载中的 StackStorage 类。 也是一个清洁的抽象,AsyncLocal <T>,这使您可以存储任何值,并将它与控制流转移到后续异步延续。 我将使用它作为因果关系链存储为 Windows 存储应用程序方案。

跟踪在 Windows 存储应用程序中的因果关系

所述的方法将仍然举起 Windows 存储方案中如果 System.Diagnostics.StackTrace API 都可用。 为好或坏,不是,这意味着您无法得到任何信息调用代码内当前上面的堆栈帧。 因此,尽管仍支持第三方物流的事件,对 TaskWaitStart 或 TaskWaitEnd 的调用是深埋在框架方法调用,所以您对您造成这些事件触发的代码没有任何信息。

幸运的是,Windows 存储区为.NET 应用程序 (以及.NET 框架 4.5) 提供了 CallerMemberNameAttribute (bit.ly/PsDH0p) 和其同行 CallerFilePathAttribute 和 CallerLine­NumberAttribute。 当装饰着这些可选方法参数时,编译器会在编译时初始化具有相应值的参数。 例如,下面的代码将输出"在行 14"c:\Full\Path\To\Program.cs 中的 main ():

static void Main(string[] args)
{
  LogCurrentFrame();
}
static void LogCurrentFrame([CallerMemberName] string name = null,
  [CallerFilePath] string path = null, 
    [CallerLineNumber] int line = 0)
{
  Console.WriteLine("{0}() in {1} at line {2}", name, path, line);
}

只有这样,要获取调用帧,这意味着您必须确保从您想要捕获的因果关系链中的所有方法调用它获取有关信息的测井方法。 一个方便的位置,为此将装饰每个等待调用扩展方法,像这样的表达式:

await WorkAsync().WithCausality();

在这里,WithCausality 方法捕获当前帧,将其追加到因果关系链和返回任务或 awaitable (取决于何种 WorkAsync 返回),原单完成后,从因果关系链中删除图文框。

可以等待多个不同的东西,应该是 WithCausality 的多个重载。 这是简单的任务 <T> (和更容易的任务):

public static Task<T> WithCausality<T>(this Task<T> task,
  [CallerMemberName] string member = null,
  [CallerFilePath] string file = null,
  [CallerLineNumber] int line = 0)
{
  var removeAction =
    AddFrameAndCreateRemoveAction(member, file, line);
  return task.ContinueWith(t => { removeAction(); return t.Result; });
}

然而,它是复杂的自定义 awaitables。 您可能知道,C# 编译器允许您等待任何遵循特定模式的类型的实例 (见 bit.ly/AmAUIF),这使得将容纳任何自定义 awaitable 不可能使用的写作重载静态只键入。 您可以为预定义的框架,如 YieldAwaitable 或 ConfiguredTaskAwaitable 中的 awaitables 的几个快捷方式重载 — — 或在您的解决方案中定义的 — — 但一般你必须求助于动态语言运行时 (DLR)。 处理所有个案需要大量的样板代码,所以想看看附带的源代码的详细信息。

此外值得注意的情况下嵌套等待、 WithCausality 方法将执行从内到外 (等待计算表达式),因此,必须注意要装配的堆栈中按正确的顺序。

查看因果关系链

这两种描述的方法的调用堆栈段或帧列表作为在内存中保留因果关系信息。 然而,走他们和串联成单一因果关系链的显示是用手做很繁琐的事。

最简单的选项来自动化这是利用调试器计算器。 在这种情况下,在一个公共的类,作者公共静态属性 (或方法),在调用时,走的存储段的列表并返回串联的因果关系链。 然后您可以在调试过程中计算此属性和文本可视化工具中查看结果。

不幸的是,这种方法不会在两种情况下工作。 一个时发生的最顶层的堆栈帧中是相当常见的情况,为调试应用程序挂起,因为基于内核的同步基元做调用到本机代码中的本机代码。 调试器计算器将只显示,"无法计算表达式,因为当前方法的代码进行了优化"(Mike 档描述这些限制在详细 bit.ly/SLlNuT)。

另一个问题是验尸的调试。 其实可以在 Visual Studio 中打开一个小型转储和令人惊讶 (鉴于有没有进程进行调试,只有其内存转储),就可以检查 (运行属性 getter) 的属性值和甚至调用某些方法 ! 这神奇的功能内置 Visual Studio 调试器和工程通过解释监视表达式,它 (在对比度到实时调试,编译后的代码获取执行所在) 调用到的所有方法。

很明显,有一些限制。 例如,同时做转储调试,您不能在任何方式调用到本机方法 (意味着你甚至不能执行的委托,因为其 Invoke 方法生成的本机代码) 或访问一些限制 Api (如 System.Reflection)。 基于解释器的评价也是预计缓慢 — — 和可悲的是,由于一个 bug,调试转储的评价超时是限于无论配置 Visual Studio 2012 年第二次 1。 这一点,因为需要遍历堆栈跟踪段的列表,并循环访问所有帧的方法调用的数目禁止为此目的的计算器使用。

幸运的是,调试器始终允许访问的字段值 (即使在调试转储或顶部堆栈帧时在本机代码中),使其得以通过构成存储的因果关系链的对象进行爬网并重建它。 这是很明显是冗长乏味的所以我写了一个 Visual Studio 扩展,这会为您 (见所附的示例代码)。 图 2 显示最后的经验是什么样子。 请注意右边的图也由该扩展生成相当于异步并行堆栈。

Causality Chain for an Asynchronous Method and “Parallel” Causality for All Threads
图 2 异步方法和"平行"的因果关系的所有线程的因果关系链

比较和警告

这两种因果关系跟踪方法不是免费的。 (调用者信息-基于) 第二个是更轻量的因为它不涉及昂贵的 StackTrace API,而依赖编译器将调用方提供帧信息在编译时,在正在运行的程序中的"免费"的手段。 但是,它仍然使用事件处理基础结构与其成本来支持 AsyncLocal <T>。 另一方面,第一种方法提供了更多的数据,不跳过帧而不等待。 它还会自动跟踪几个等待基于任务的异步凡没有出现其他情况,如 Task.Run 方法 ; 另一方面,它不能与使用自定义的 awaitables。

TPL 基于事件的跟踪器的另一个好处是,现有的异步代码不需要修改,而对的调用方信息基于属性的方法,则需要更改每个等待您的程序中的语句。 但只有后者支持 Windows 存储应用程序。

TPL 事件跟踪程序也患有大量的样板框架代码的堆栈跟踪的细分,虽然它可以轻松地筛选由框架的命名空间或类的名称。 请参阅有关的常用的筛选器列表的代码示例。

另一个涉及异步代码中的循环。 请考虑下面的代码段:

async static Task Loop()
{
  for (int i = 0; i < 10; i++)
  {
    await FirstAsync();
    await SecondAsync();
    await ThirdAsync();
  }
}

由方法的末尾,其因果关系链会增长到超过 30 段、 FirstAsync、 SecondAsync 和 ThirdAsync 的帧之间反复交替。 对于有限循环,这可能是可以忍受的尽管它仍然是浪费内存来存储重复帧 10 倍。 但是,在某些情况下,一个程序可能引入有效的无限循环,例如,在一个消息循环的情况下。 此外,无限重复可能会引入不循环或等待构造 — — 本身重新上每个刻度线计时器是一个完美的例子。 跟踪无限的因果关系链是运行内存不足,因此存储的数据量已经以某种方式将减至一个有限的数额确定方法。

这一问题并不影响基于调用方的信息追踪器中,因为它帧从列表中删除后立即开始的延续。 有两种 (可组合) 的方法来修复此问题为第三方物流活动方案。 一是要削减较旧的数据基于滚动的最大存储量。 另一个是有效地代表循环和避免重复。 两种做法,也可能检测常见的无限循环模式和削减的因果关系链明确这些点。

随意请参阅随附的示例项目,以查看如何执行循环折叠。

如上所述,第三方物流事件 API 仅允许您捕获一条因果关系链,不图。 这是因为 Task.WaitAll 和 Task.WhenAll 方法实现为倒计时,只有当最后一项任务来完成,该计数器变为零,安排延续。 因此,只有最后一个完成的任务形成因果关系链。

总结

在本文中,您已经了解调用堆栈、 返回堆栈和因果关系链之间的差异。 现在应该意识到,.NET 框架提供了跟踪调度的扩展点和异步操作的执行,能够利用这些来捕获和保留的因果关系链。 方法描述盖住跟踪中经典和 Windows 存储区的应用程序,在因果关系和验尸调试方案。 您还学习了有关异步本地存储的概念和其可能的实现用于 Windows 存储区的应用程序。

现在去和纳入跟踪到异步代码库的因果关系或使用异步本地存储区中并行计算 ; 探讨.NET 框架 4.5 和.NET 为 Windows 存储应用程序提供建造一些新的例如跟踪程序在您的程序 ; 未完成任务的事件源 或使用此扩展点来激发您自己的事件来调整应用程序的性能。

Andriy Stasyuk (Andrew) 是在 Microsoft 托管语言团队测试二、 软件开发工程师。他有七年的经验作为参与者、 任务作者、 陪审团成员和在不同的国家和国际编程竞赛教练。他在 Paladyne/Broadridge 财务解决方案公司的财务软件开发工作 和德意志银行才搬到 Microsoft。他在节目中的主要利益是算法、 并行和皮包公司。

衷心感谢以下技术专家对本文的审阅:万斯摩理臣和卢西恩 Wischik