在等待异步函数时仅取消内部任务?

时间:2013-06-11 09:31:21

标签: c# task async-await yield-return

我正在尝试扩展一个框架,我们的框架用户可以使用C#作为一种具有非阻塞功能的语言(又名“coroutines”)。我的第一个设计使用yield return语句和IEnumerator作为函数返回值。

然而,如果你试图调用另一个让步函数(如果另一个函数应该返回一个值,则更容易出错)并且使用起来很麻烦。

因此,我想要使用asyncawait来提供corountines。结果语法会非常有趣。

在我们的框架中,我需要确保这些非阻塞脚本中没有两个并行运行,因此我编写了自己的SynchonizationContext来自行安排所有操作。这就像一个魅力。

现在更有趣的东西:我们设计的另一个核心部分是,用户可以注册某种“检查功能”,如果它们失败则不会继续“当前”执行功能。每次非阻塞功能恢复时,都应执行检查功能。

例如:

async Task DoSomething()
{
    var someObject = SomeService.Get(...);
    using (var check = new SanityCheckScope())
    {
        check.StopWhen(() => someObject.Lifetime < 0);

        ...

        await SomeOtherStuff();
        // when execution comes back in here, the StopWhen - condition above should be
        // evaluated and the Task should not be scheduled if its true.

        ...
    }
}

每当SomeOtherStuff或其调用的任何异步函数恢复时,应检查已注册的条件,并且如果任何条件为真,DoSomething应该停止。

为了实现这一点,我的SynchonizationContext检查已注册的函数(通过CallContext.LogicalSetData传递),如果返回true则不安排任务。

现在出现了棘手的问题。假设函数SomeOtherStuff如下所示:

async Task SomeOtherStuff()
{
    using (var check = new SanityCheckScope())
    {
        // register my own check functions
        await Whatever();
    }
}

在示例SomeOtherStuff中注册了自己的检查功能。如果在await Whatever()之后它们为真,则自然只应停止SomeOtherStuff函数。 (假设如果SomeOtherStuff返回Task<XXX>,那么如果将default(XXX)用作返回值,那么对我来说没问题。)

我怎么能接近这个?我无法进入原始函数await SomeOtherStuff();之后的Action。在await的开头似乎没有钩子或回调(无论如何都没有意义)。

另一个想法是抛出异常而不是“不安排”任务。相反(或者除了)using - 块,我会写一个try / catch。但是,我遇到的问题是,如果原始 DoSomething的任何检查功能在内部await Whatever();之后失败,我将如何停止DoSomething(与{{一起) 1}})?

所以,在我解决所有这些问题之前,请回到我的SomeOtherStuffIEnumerator。任何人都知道这是否可以由yield return / async框架完成?

2 个答案:

答案 0 :(得分:2)

我的建议是使用TPL Dataflow并完成它。您滥用语言功能的次数越多,例如async(或IEnumerable),您的代码就越难以维护。

那说......

你做的(有点)在await周围有一些钩子:定制等待。 Stephen Toub有一个blog on them,Jon Skeet在他的eduasync series中介绍了一些细节。

但是,您无法从async方法返回自定义等待,因此这种方法意味着您的所有await都必须“选择加入”“检查”行为:

async Task DoSomething()
{
  var someObject = SomeService.Get(...);
  using (var check = new SanityCheckScope())
  {
    check.StopWhen(() => someObject.Lifetime < 0);
    await SomeOtherStuff().WithChecks();
  }
}

当检查失败时,我不清楚语义应该是什么。从您的问题来看,听起来好像一旦检查失败,您只想让该方法中止,因此检查更像是代码合同而不是监视器或条件变量。听起来,范围应仅适用于该方法,而不适用于从该方法调用的方法。

您可以通过使用自定义awaiter获取工作,该自定义awaiter封装返回的Task并使用TaskCompletionSource<T>来公开不同的Task。自定义等待程序将在执行async方法的继续之前执行检查。

需要注意的另一件事是你的“范围”如何运作。考虑一下这种情况:

async Task DoSomething()
{
  using (var check = new SanityCheckScope())
  {
    check.StopWhen(...);

    await SomeOtherStuff();

    await MoreOtherStuff();
  }
}

或者更有趣:

async Task DoSomething()
{
  using (var check = new SanityCheckScope())
  {
    check.StopWhen(...);

    await SomeOtherStuff();

    await Task.WhenAll(MoreOtherStuff(), MoreOtherStuff());
  }
}

我怀疑您的范围必须使用AsyncLocal<T> (described on my blog)才能正常工作。

答案 1 :(得分:0)

您是否考虑过实施自己的SynchronizationContext?您可以实现自己的(假设没有安装任何有意义的SynchronizationContext),或者您可以在现有上下文周围安装包装器。

await语法将继续发布到SynchronizationContext(假设await未配置这样做),这将允许您拦截它。当然,任何异步回调都将发布到SynchronizationContext,因此可能无法直接检测到连续调用的确切时刻,因此这可能会导致对发布到上下文的所有回调进行完整性检查。

您的SanityCheckScope可以确保使用SynchronizationContext以嵌套方式注册检查范围,显然,类的IDisposable部分会取消注册并恢复父范围。这只有在您没有可以拥有多个并行子范围的情况下才有效。

据我所知,停止执行的唯一方法是抛出某种异常。