在开发一些异步代码时,我和同事们遇到了一个奇怪的问题。一个组件以连续的速率触发,但我们希望其中调用的一个依赖项永远不会同时执行多个任务。
我们决定只是缓存任务,没有问题。我们在缓存任务的赋值周围加了一个锁,并让单例任务在完成后删除自身。
它失败了!尽管是微妙的。清除onlyOneTask的那一行在任务分配之前执行!之后,onlyOneTask中始终存在一个任务实例,并且它永远不会重新执行工作。
你可能认为这应该会死锁,但是线程可以重新获取锁定,并且由于异步代码可以将自身降级为完全同步代码,因此第二个锁在同一线程上被获取。没有死锁。
如果SomeWork在执行过程中某个时候产生yield,则似乎表现符合我们的期望,但它确实引发了一些关于其精确行为的问题。我不确定异步代码在以同步方式和异步方式执行时线程和锁定如何交互的确切细节。
我想指出的另一件事是,通过使用Task.Run来强制在其自己的任务上运行工作似乎可以修复此问题。但是有一个主要问题:这段代码旨在在.NET服务中运行,在那里使用Task.Run是不好的形式。
我们决定只是缓存任务,没有问题。我们在缓存任务的赋值周围加了一个锁,并让单例任务在完成后删除自身。
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()调用中的某些操作会产生yield。但是,如果某些操作看起来像这样:
public Task SomeWork()
{
return Task.FromResult(0);
}
它失败了!尽管是微妙的。清除onlyOneTask的那一行在任务分配之前执行!之后,onlyOneTask中始终存在一个任务实例,并且它永远不会重新执行工作。
你可能认为这应该会死锁,但是线程可以重新获取锁定,并且由于异步代码可以将自身降级为完全同步代码,因此第二个锁在同一线程上被获取。没有死锁。
如果SomeWork在执行过程中某个时候产生yield,则似乎表现符合我们的期望,但它确实引发了一些关于其精确行为的问题。我不确定异步代码在以同步方式和异步方式执行时线程和锁定如何交互的确切细节。
我想指出的另一件事是,通过使用Task.Run来强制在其自己的任务上运行工作似乎可以修复此问题。但是有一个主要问题:这段代码旨在在.NET服务中运行,在那里使用Task.Run是不好的形式。
有一种看似解决这个问题的方法,即强制Task始终表现得像是有异步实现一样。我想这是不可能的,但还是问一下吧。
那么,问题来了:
- 即使SomeWork()进行了yield,这种情况是否会发生死锁?
- 如果TaskScheduler在同一线程上继续执行,在SomeWork()足够快的情况下,异步案例是否会像同步案例一样(在设置onlyOneTask之前清除)?
- 有没有一种方法可以强制现有任务始终表现为它是异步的,而不会影响线程池?
- 是否有更好的替代方案?
值得一提的是,我们发现如果只是检查缓存任务的状态并确定何时再次启动SomeWork(),我们的问题就消失了,所以这主要是学术性质。
await SomeWork();
之前加上await Task.Yield()
吗? - StriplingWarrior