第二个线程在第一个线程释放之前进入锁定

时间:2015-12-15 16:59:11

标签: c# multithreading locking task-parallel-library

我正在使用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有什么关系吗?

3 个答案:

答案 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();
        }
    }