我正在尝试扩展一个框架,我们的框架用户可以使用C#作为一种具有非阻塞功能的语言(又名“coroutines”)。我的第一个设计使用yield return
语句和IEnumerator作为函数返回值。
然而,如果你试图调用另一个让步函数(如果另一个函数应该返回一个值,则更容易出错)并且使用起来很麻烦。
因此,我想要使用async
和await
来提供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}})?
所以,在我解决所有这些问题之前,请回到我的SomeOtherStuff
和IEnumerator
。任何人都知道这是否可以由yield return
/ async
框架完成?
答案 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部分会取消注册并恢复父范围。这只有在您没有可以拥有多个并行子范围的情况下才有效。
据我所知,停止执行的唯一方法是抛出某种异常。