【问题标题】:Lock-free guard for synchronized acquire/release同步获取/释放的无锁保护
【发布时间】:2012-05-10 15:26:08
【问题描述】:

我有一个共享的临时文件资源,它被分成 4K 的块(或一些这样的值)。文件中的每个 4K 都由一个从零开始的索引表示。对于这个共享资源,我跟踪正在使用的 4K 块索引,并始终返回索引最低的未使用的 4K 块,如果全部都在使用,则返回 -1。

这个索引的 ResourceSet 类有一个公共的获取和释放方法,两者都使用同步锁,其持续时间大约类似于生成 4 个随机数的时间(昂贵,cpu-wise)。

因此从后面的代码可以看出,我在acquire()上使用了一个AtomicInteger“计数信号量”来防止大量线程同时进入临界区,返回-1(不可用对现在)如果线程太多。

目前,我使用 100 的常量用于紧密的 CAS 循环以尝试增加获取中的原子整数,并使用 10 的常量用于允许进入临界区的最大线程数,这足够长制造争用。我的问题是,对于有多个线程试图访问这些 4K 块的中等到高负载的 servlet 引擎,这些常量应该是什么?

public class ResourceSet {

    // ??? what should this be
    // maximum number of attempts to try to increment with CAS on acquire
    private static final int    CAS_MAX_ATTEMPTS = 50;

    // ??? what should this be
    // maximum number of threads contending for lock before returning -1 on acquire
    private static final int    CONTENTION_MAX = 10;

    private AtomicInteger        latch = new AtomicInteger(0);

    ... member variables to track free resources

    private boolean aquireLatchForAquire ()
    {
        for (int i = 0; i < CAS_MAX_ATTEMPTS; i++) {
            int val = latch.get();
            if (val == -1)
                throw new AssertionError("bug in ResourceSet");        // this means more threads than can exist on any system, so its a bug!
            if (!latch.compareAndSet(val, val+1))
                continue;
            if (val < 0 || val >= CONTENTION_MAX) {
                latch.decrementAndGet();
                // added to fix BUG that comment pointed out, thanks!
                return false;
            }
        }
        return false;
    }

    private void aquireLatchForRelease ()
    {
        do {
            int val = latch.get();
            if (val == -1)
                throw new AssertionError("bug in ResourceSet");    // this means more threads than can exist on any system, so its a bug!
            if (latch.compareAndSet(val, val+1))
                return;
        } while (true);
    }

    public ResourceSet (int totalResources)
    {
        ... initialize
    }

    public int acquire (ResourceTracker owned)
    {        
        if (!aquireLatchForAquire())
            return -1;

        try {
            synchronized (this) {
                ... algorithm to compute minimum free resoource or return -1 if all in use
                return resourceindex;
            }
        } finally {
            latch.decrementAndGet();
        }
    }

    public boolean release (ResourceIter iter)
    {
        aquireLatchForRelease();
        try {
            synchronized (this) {
                ... iterate and release all resources
            }
        } finally {
            latch.decrementAndGet();
        }
    }
}

【问题讨论】:

  • 你有代码要分享吗?
  • 我刚刚添加了代码来显示我在做什么。
  • 请写的重点准确。
  • 在达到 contention_max 后,此代码是否会出现错误,因为该方法将返回 false,然后从不调用递减。
  • @benmmurphy -- 很棒的电话,让我在测试中省去了很多痛苦!

标签: java multithreading mutex critical-section lock-free


【解决方案1】:

编写一个良好且高性能的自旋锁实际上非常复杂,需要对内存屏障有很好的理解。仅仅选择一个常数并不会削减它,而且绝对不会是可移植的。 Google 的 gperftools 有 an example,您可以查看它,但可能比您需要的要复杂得多。

如果您真的想减少对锁的争用,您可能需要考虑使用更细粒度和更乐观的方案。一个简单的方法是将您的块分成 n 个组,并为每个组关联一个锁(也称为剥离)。这将有助于减少争用并提高吞吐量,但无助于减少延迟。您还可以将 AtomicBoolean 关联到每个块和 CAS 以获取它(在失败的情况下重试)。在处理无锁算法时要小心,因为它们往往很难正确处理。如果你做对了,它可以大大减少获取块的延迟。

请注意,如果不知道您的块选择算法是什么样的,就很难提出更细粒度的方法。我还假设您确实存在性能问题(已对其进行了分析以及所有内容)。

当我这样做时,您的自旋锁实现存在缺陷。您永远不应该直接在 CAS 上旋转,因为您正在向内存屏障发送垃圾邮件。对于任何严重的争用(与thundering-herd problem 相关),这将非常缓慢。最低要求是在您的 CAS 之前首先检查变量的可用性(如果没有障碍读取就很简单)。更好的是不要让所有线程都以相同的值旋转。这应该可以避免相关的缓存线在您的内核之间进行乒乓操作。

请注意,我不知道 Java 中的原子操作与哪种类型的内存屏障相关联,因此我的上述建议可能不是最佳或正确的。

最后,The Art Of Multiprocessor Programming 是一本有趣的书,可以更好地了解我在这个答案中吐出的所有废话。

【讨论】:

  • 你的回答让我想知道,如果我的 CAS 循环导致内存障碍成为性能问题,那么 java.util.concurrent.Semaphore 如何为 decrementAndGet 等做自旋锁。
  • 我检查了 JDK 源代码,getAndIncrement 和我的一样使用 CAS 循环,只是它永远不会结束。
  • (希望我能在超过 5 分钟后编辑 cmets。)我的意思是 AtomicInteger getAndIncrement() 使用与我相同的 CAS 循环,只是它是无限的。因此,如果我的 CAS 循环导致内存屏障垃圾邮件,那么 AtomicInteger 的 CAS 循环也会导致增量和减量。
【解决方案2】:

我不确定是否有必要为此场景创建自己的 Lock 类。由于 JDK 提供了 ReentrantLock,它在获取锁时也利用了 CAS 指令。与您的个人 Lock 类相比,性能应该相当不错。

【讨论】:

  • ReentrantLock 不足以解决这个问题。具有 10 个许可的信号量可以模拟我的代码,但我无法控制 CAS 循环。 Semaphore 类的 CAS 循环会一直循环,直到获得锁为止。
  • 是的,ReentrantLock 无法解决问题。对不起,我的误解。另一方面,如果许可用完,Semaphone 会调用 LockSupport.park(this) 来暂停当前线程。在我看来这是正确的行为,因为它可以节省 CPU 忙于重试以获得许可。
【解决方案3】:

如果您希望您的线程在no resource available 上犹豫不决,您可以使用SemaphoretryAcquire 方法。

我会简单地将您的 synchronized 关键字替换为 ReentrantLock 并在其上使用 tryLock() 方法。如果你想让你的线程稍等片刻,你可以在同一个类上使用tryLock(timeout)。选择哪一个,使用什么超时值,需要通过性能测试来确定。

创建一个明确的门似乎对我来说似乎没有必要。我并不是说它永远无济于事,但 IMO 它实际上更有可能损害性能,而且肯定会增加复杂性。因此,除非您在这里遇到性能问题(基于您所做的测试)并且您发现这种门控有帮助,否则我建议您使用最简单的实现。

【讨论】:

  • 我检查了 Semaphore.tryAcquire(long timeout, TimeUnit) 的 JDK 代码,当它无法立即获得锁时,它会在最紧凑的循环中调用 System.nanoTime()。我在我的系统上分析了 System.nanoTime(),它花了半微秒,而我试图用 CAS 保护的关键部分比 System.nanoTime 快 10 倍!因此我认为 Semaphore 和 ReentrantLock 的开销太大了。
  • @AndyNuss:事实上,这可能是有道理的,因为当您有一段时间无法成功进行 CAS 时,将 CPU 让给其他线程通常是有意义的。另一方面,如果您的关键部分速度如此之快,那么一开始就使用犹豫模式可能是错误的。为什么不简单地使用synchronized 并让线程等到它们得到他们想要的东西?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-23
  • 1970-01-01
相关资源
最近更新 更多