我正在使用C#中的一些线程构造,遇到了一些无法理解锁定工作原理的东西。我有一个接受异步任务的辅助函数,并且在多次调用时使用TaskCompletionSource
成员尝试同步访问。
public static void Main(string[] args)
{
var test = new TestClass();
var task1 = test.Execute("First Task", async () => await Task.Delay(1000));
var task2 = test.Execute("Second Task", async () => await Task.Delay(1000));
task1.Wait();
task2.Wait();
Console.ReadLine();
}
class TestClass : IDisposable
{
private readonly object _lockObject = new object();
private TaskCompletionSource<bool> _activeTaskCompletionSource;
public async Task Execute(string source, Func<Task> actionToExecute)
{
Task activeTask = null;
lock (_lockObject)
{
if (_activeTaskCompletionSource != null)
{
activeTask = _activeTaskCompletionSource.Task;
}
else
{
_activeTaskCompletionSource = new TaskCompletionSource<bool>();
}
}
while (activeTask != null)
{
await activeTask;
lock (_lockObject)
{
if (_activeTaskCompletionSource != null)
{
activeTask = _activeTaskCompletionSource.Task;
}
else
{
activeTask = null;
}
}
}
await actionToExecute();
lock (_lockObject)
{
_activeTaskCompletionSource.SetResult(true);
_activeTaskCompletionSource = null;
}
}
}
这总是最终落入第二个任务的无限循环。我发布了一些代码来记录每一步,它总是产生这样的东西(我在#s之后手动插入了注释):
[First Task] Waiting for lock (setup) [First Task] Entered lock (setup) [First Task] Grabbing '_activeTaskCompletionSource' (setup) [First Task] Lock released (setup) [First Task] RUNNING ... [Second Task] Waiting for lock (setup) [Second Task] Entered lock (setup) [Second Task] Assigning 'activeTask' (setup) [Second Task] Lock released (setup) [Second Task] Waiting for task to complete ... [First Task] COMPLETED! [First Task] Waiting for lock (cleanup) [First Task] Entered lock (cleanup) [First Task] Setting _activeTaskCompletionSource result ... # Never gets to '_activeTaskCompletionSource = null' # Never gets to 'Releasing lock (cleanup)' for first task [Second Task] Awaited task completed! [Second Task] Waiting for lock (loop) # Immediately enters lock after 'await' is complete # Does not wait for 'First Task' to finish its lock! [Second Task] Entered lock (loop) [Second Task] Assigning 'activeTask' (loop) [Second Task] Lock released (loop) [Second Task] Waiting for task to complete ... [Second Task] Awaited task completed!
这最终将第二个任务发送到无限循环,因为_activeTaskCompletionSource
永远不会被设置回null
。
我的印象是没有其他线程可以在所有先前的线程退出之前进入锁定,但是在这里,我的First Task
线程永远不会完成并释放其清理锁定之前Second Task
线程抓住了它。
这与混音锁和async / await有什么关系吗?
答案 0 :(得分:3)
调用TaskCompletionSource.SetResult
可以内联延续,从而导致意外和任意代码在锁定下运行。 await
也使用延续。
这种令人讨厌的行为是TPL中的一个设计错误。如果你关心这个there is a GitHub issue。在那里发表评论。
答案 1 :(得分:2)
作为boot4Life points out,这是一个框架的设计错误,允许SetResult
的延续在同一个线程上运行。
如果您使用的是.NET 4.6,请将完成源的创建更改为
_activeTaskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
这可以防止它发生。如果您只使用4.5.x,则必须在新线程上运行完成以防止它被拾取。
lock (_lockObject)
{
var completionSource = _activeTaskCompletionSource;
_activeTaskCompletionSource = null;
Task.Run(() => completionSource.SetResult(true));
}
答案 2 :(得分:1)
尝试SemaphorSlim。对于基于任务的异步方案,lock语句不是正确的工具。
此外,还有一种诱惑来等待外部已在类中进行的任务,但是我的理解是,如果您这样做,除非正确使用ConfigureAwait,否则将不会等待它们。信号量苗条解决了这个问题,因为据我所知,无论如何它都会等待锁。
private readonly SemaphoreSlim _RefreshLock = new SemaphoreSlim(1);
public virtual async Task RefreshAsync()
{
try
{
await _RefreshLock.WaitAsync();
//Your work here
}
finally
{
_RefreshLock.Release();
}
}