【问题标题】:Second thread enters lock before first thread releases it第二个线程在第一个线程释放它之前进入锁
【发布时间】:2016-03-21 14:09:47
【问题描述】:

我正在使用 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之后手动插入了cmets):

[第一个任务]等待锁定(设置) [第一个任务]进入锁(设置) [第一个任务]抓取'_activeTaskCompletionSource'(设置) [第一个任务] 解锁(设置) [第一个任务]正在运行... [第二个任务] 等待锁定(设置) [第二个任务] 进入锁(设置) [第二个任务] 分配“activeTask”(设置) [第二个任务] 解锁(设置) [第二个任务] 等待任务完成... 【第一个任务】完成! 【第一个任务】等待锁(清理) 【第一个任务】进入锁(清理) [第一个任务]设置_activeTaskCompletionSource结果... # 永远不会到达'_activeTaskCompletionSource = null' # 第一个任务永远不会“释放锁(清理)” 【第二个任务】等待的任务完成! 【第二个任务】等待锁(循环) # 'await' 完成后立即进入锁 # 不等待“第一个任务”完成锁定! 【第二个任务】进入锁(循环) [第二个任务]分配'activeTask'(循环) [第二个任务] 锁释放(循环) [第二个任务] 等待任务完成... 【第二个任务】等待的任务完成!

这最终将第二个任务送入无限循环,因为_activeTaskCompletionSource 永远不会设置回null

我的印象是 没有其他线程 可以进入锁,直到所有先前的线程都退出它,但在这里,我的 First Task 线程永远不会完成并在 @ 之前释放它的清理锁987654326@线程抓住了它。

这和混合锁和异步/等待有什么关系吗?

【问题讨论】:

  • 绝对不建议将 task.Wait() 或 task.Result 设置为异步任务,因为它很容易陷入死锁情况。
  • @OsmanEsen 使用此示例中的task.Wait() 调用只是因为它在控制台应用程序中运行,而在主函数中不可能使用async/await。我已经避免在所有异步函数本身以及整个TestClass 实现中使用阻塞调用。

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


【解决方案1】:

调用TaskCompletionSource.SetResult 可以内联延续导致意外和任意代码在锁定下运行。 await 也使用延续。

这种讨厌的行为是 TPL 中的一个设计错误。如果你关心这个there is a GitHub issue。在那里发表评论。

【讨论】:

  • 特别是如果您使用 .NET 4.6,您可以通过将完成源的创建更改为 _activeTaskCompletionSource = new TaskCompletionSource&lt;bool&gt;(TaskCreationOptions.RunContinuationsAsynchronously); 来解决它
  • 哎哟!那很痛苦。我认为这是我做错了什么,而不是框架本身的问题。感谢您的信息。
【解决方案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));            
    }

【讨论】:

  • 不幸的是我被困在 4.5 上,所以看起来你需要在自己的线程上运行它的解决方法。
【解决方案3】:

试试 SemaphorSlim。对于基于任务的异步场景,lock 语句不是正确的工具。

此外,有一种诱惑是等待已经在外部类中进行的任务,但我的理解是,如果你这样做,除非你正确使用 ConfigureAwait,否则它们不会被等待。 Semaphore slim 解决了这个问题,因为据我所知,它无论如何都会等待锁。

    private readonly SemaphoreSlim _RefreshLock = new SemaphoreSlim(1);

    public virtual async Task RefreshAsync()
    {
        try
        {
            await _RefreshLock.WaitAsync();

            //Your work here

        }
        finally
        {
            _RefreshLock.Release();
        }
    }

【讨论】:

  • 谢谢,我不知道这存在。我将不得不检查它以了解未来的异步/等待内容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多