【问题标题】:Lock-free, awaitable, exclusive access methods无锁、等待、独占访问方法
【发布时间】:2015-05-26 17:31:58
【问题描述】:

我有一个线程安全类,它使用需要以独占方式访问的特定资源。在我的评估中,让各种方法的调用者阻塞 Monitor.Enter 或等待 SemaphoreSlim 以访问此资源是没有意义的。

例如,我有一些“昂贵”的异步初始化。由于多次初始化没有意义,无论是来自多个线程还是单个线程,多个调用都应该立即返回(甚至抛出异常)。相反,应该创建、初始化并然后将实例分发到多个线程。

更新 1:

MyClass 在任一方向使用两个NamedPipesInitBeforeDistribute 方法并不是真正的初始化,而是正确地建立了双向连接。在设置连接之前使管道可用于N 线程是没有意义的。一旦设置好多个线程可以发布工作,但实际上只有一个线程可以读取/写入流。我很抱歉用糟糕的示例命名混淆了这一点。

更新 2:

如果InitBeforeDistribute 使用适当的等待逻辑实现了SemaphoreSlim(1, 1)(而不是互锁操作引发异常),那么 Add/Do Square 方法是否可行?它不会抛出冗余异常(如InitBeforeDistribute),同时是无锁的?

以下是一个坏的例子:

class MyClass
{
    private int m_isIniting = 0; // exclusive access "lock"
    private volatile bool vm_isInited = false; // vol. because other methods will read it

    public async Task InitBeforeDistribute()
    {
        if (Interlocked.Exchange(ref this.m_isIniting, -1) != 0)
            throw new InvalidOperationException(
                "Cannot init concurrently! Did you distribute before init was finished?");

        try
        {
            if (this.vm_isInited)
                return;

            await Task.Delay(5000)      // init asynchronously
                .ConfigureAwait(false);

            this.vm_isInited = true;
        }
        finally
        {
            Interlocked.Exchange(ref this.m_isConnecting, 0);
        }
    }
}

几点:

  1. 如果存在阻塞/等待对锁的访问导致 完美的感觉,那么这个例子就没有意义(也就是说)。
  2. 由于我需要在方法中等待,我必须使用类似 SemaphoreSlim 如果我在哪里使用“正确”的锁。放弃 上面例子的信号量让我不用担心 一旦我完成了课程,就处理它。 (我一直不喜欢 处理多个线程使用的项目的想法。这是一个次要 肯定的。)
  3. 如果经常调用该方法,可能会有一些性能 好处,当然应该衡量。

上面的例子在参考文献中没有意义。到 (3.) 所以这里是另一个例子:

class MyClass
{
    private volatile bool vm_isInited = false; // see above example
    private int m_isWorking = 0; // exclusive access "lock"
    private readonly ConcurrentQueue<Tuple<int, TaskCompletionSource<int>> m_squareWork =
        new ConcurrentQueue<Tuple<int, TaskCompletionSource<int>>();

    public Task<int> AddSquare(int number)
    {
        if (!this.vm_isInited) // see above example
            throw new InvalidOperationException(
                "You forgot to init! Did you already distribute?");

        var work = new Tuple<int, TaskCompletionSource<int>(number, new TaskCompletionSource<int>()
        this.m_squareWork.Enqueue(work);

        Task do = DoSquare();

        return work.Item2.Task;
    }

    private async Task DoSquare()
    {
        if (Interlocked.Exchange(ref this.m_isWorking, -1) != 0)
            return; // let someone else do the work for you

        do
        {
            try
            {
                Tuple<int, TaskCompletionSource<int> work;

                while (this.m_squareWork.TryDequeue(out work))
                {
                    await Task.Delay(5000)      // Limiting resource that can only be
                        .ConfigureAwait(false); // used by one thread at a time.

                    work.Item2.TrySetResult(work.Item1 * work.Item1);
                }
            }
            finally
            {
                Interlocked.Exchange(ref this.m_isWorking, 0);
            }
        } while (this.m_squareWork.Count != 0 &&
            Interlocked.Exchange(ref this.m_isWorking, -1) == 0)
    }
}

我应该注意这个“无锁”示例的某些特定负面方面吗?

大多数与 SO 上的“无锁”代码有关的问题通常都反对它,并指出它是针对“专家”的。很少(我可能错了)我会看到可以深入研究的书籍/博客/等建议,如果有人愿意的话。如果我应该研究任何此类资源,请分享。任何建议将不胜感激!

【问题讨论】:

  • 您有什么具体问题吗?
  • 所以,为了确保我理解你......你有一个昂贵的初始化过程,当第一个线程访问资源时调用它。尝试访问资源的后续线程的用例是什么?从您的帖子中,听起来您希望它们抛出异常或立即返回(不获取数据?),这两种方法听起来都不比在初始化完成之前简单地阻塞更合适。我错过了什么吗?当某事需要独占访问时,这听起来像是锁的确切用例。
  • AsyncLock 是你的朋友。不要想太多。
  • 我可能在这里遗漏了一些东西,但是您为什么不简单地使用Lazy&lt;T&gt; 进行线程安全初始化?
  • 顺便说一句,我想不出一个用例可以同时使用“无锁”和“异步”。

标签: c# multithreading asynchronous lock-free interlocked


【解决方案1】:

更新:很棒的文章相关

.: Creating High-Performance Locks and Lock-free Code (for .NET) :.


  1. lock-free 算法的重点不是它们是为experts 设计的。
    重点是Do you really need lock-free algorythm here?这里看不懂你的逻辑:

    由于多次初始化没有意义,无论是来自多个线程还是单个线程,多次调用都应该立即返回(甚至抛出异常)。

    为什么您的用户不能简单地等待初始化结果,然后再使用您的资源?如果可以的话,只需使用Lazy&lt;T&gt; 类甚至Asynchronous Lazy Initialization

  2. 您真的应该阅读consensus numberCAS-operations 以及为什么在实现您自己的同步原语时这很重要。

    在您的代码中,您使用的是Interlocked.Exchange 方法,它实际上不是CAS,因为它总是 交换值,并且它的共识数等于2 .这意味着使用这种构造的原语仅适用于2 线程(不是在您的情况下,但仍然是2)。

    我试图定义您的代码是否适用于3 线程,或者在某些情况下会导致您的应用程序进入损坏状态,但在30 分钟后我停止了。在尝试理解您的代码一段时间后,您的任何团队成员都会像我一样停下来。这是浪费时间,不仅是你的,也是你的团队的。除非真的需要,否则不要重新发明轮子。

  3. 我在相关领域最喜欢的书是 Ben Watson 的 Writing High-Performance .NET Code,我最喜欢的博客是 Stephen Cleary's。如果您可以更具体地说明您对哪种书感兴趣,我可以添加更多参考资料。

  4. 程序中没有锁定不会使您的应用程序lock-free。在 .NET 应用程序中,您真的不应该将Exceptions 用于您的内部程序流程。考虑到操作系统暂时没有安排初始化线程(出于各种原因,无论它们究竟是什么原因)。

    在这种情况下,您应用中的所有其他线程将在尝试访问您的共享资源时逐步死亡。我不能说这是lock-free 代码。是的,它没有锁,但是它不能保证程序的正确性,因此它不是无锁的

【讨论】:

  • 谢谢!我想大致了解这些东西(线程/性能),这非常有用。
【解决方案2】:

Maurice Herlihy 和 Nir ​​Shavit 的《多处理器编程艺术》,是无锁和无等待编程的绝佳资源。无锁是一种进度保证,而不是一种编程模式,因此要证明算法是无锁的,必须验证或展示进度保证的证明。简单来说,无锁意味着阻塞或停止一个线程不会阻塞其他线程的进程,或者如果一个线程经常被无限阻塞,那么另一个线程会无限频繁地进行进程。

【讨论】:

  • 推荐的书不是 .NET 特定的,这对更广泛的理解是积极的。谢谢!
猜你喜欢
  • 1970-01-01
  • 2016-08-05
  • 1970-01-01
  • 1970-01-01
  • 2012-01-30
  • 2018-03-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多