【发布时间】:2016-10-18 15:18:27
【问题描述】:
在开发一些异步代码时,我和我的同事在部分代码中遇到了一个奇怪的问题。一个组件以连续的速率触发,但我们希望它调用的其中一个依赖项一次永远不会执行多个任务。
我们决定只缓存任务,没问题。我们对缓存任务的分配进行了锁定,并让单例任务在完成后自行删除。
Task onlyOneTask;
public Task TryToBeginSomeProcess()
{
lock (someLock)
{
if (onlyOneTask == null)
{
onlyOneTask = DoSomethingButOnlyOneAtATime();
}
return onlyOneTask;
}
}
public async Task DoSomethingButOnlyOneAtATime()
{
// do some work
await SomeWork();
lock (someLock)
{
onlyOneTask = null;
}
}
所以,这实际上似乎有效......假设 SomeWork() 调用中的某些内容实际上产生了。但是,如果某些工作看起来像这样:
public Task SomeWork()
{
return Task.FromResult(0);
}
失败了!然而,微妙地如此。清除 onlyOneTask 的行在分配任务之前执行!之后,onlyOneTask 中总会有一个任务实例,并且它永远不会重新执行该工作。
您可能认为这应该死锁,但是threads may reacquire locks,并且由于异步代码可能会将自身降级为完全同步代码,因此第二个锁是在同一个线程上获取的。没有死锁。
如果 SomeWork 在其执行的某个时刻产生了结果,那么它的行为似乎符合我们的预期,但它确实引发了一些关于其确切行为的问题。我不确定当异步代码同步执行与异步执行时线程和锁如何交互的确切细节。
我想指出的另一件事是,这似乎可以通过使用 Task.Run 来解决,以强制工作在自己的任务上运行。这有一个主要问题:此代码旨在在 .NET 服务中运行,而it's bad form to use Task.Run there。
一种看似解决此问题的方法是,是否有某种方法可以强制任务始终表现得像它具有异步实现一样。我想它不存在,但我不妨问问。
所以,关于这个问题:
- 即使 SomeWork() 产生,这种情况也会死锁吗?
- 如果 TaskScheduler 在同一个线程上继续运行并且 SomeWork() 足够快,那么异步情况能否像同步情况一样(在设置之前清除 onlyOneTask)?
- 有没有办法强制现有任务始终表现得好像它是异步的,而不会影响线程池?
- 有更好的选择吗?
如果我们只是检查缓存任务的状态以确定何时适合再次启动 SomeWork(),那么我们发现的价值是什么,我们的问题就消失了,所以这主要是学术性的。
【问题讨论】:
-
“某种方式来强制一个任务总是表现得像一个异步实现”:你试过
await Task.Yield()吗? (就在await SomeWork();之前) -
直到现在我还没有,看来应该可以了!