用于同步采集/释放的无锁保护



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

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

因此,从下面的代码中可以看出,我使用 AtomicInteger "计数信号量"来防止大量线程在 acquire(( 上同时进入关键部分,如果线程太多,则返回 -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();
        }
    }
}

编写一个好的、高性能的自旋锁实际上非常复杂,需要对内存障碍有很好的理解。仅仅选择一个常数不会削减它,而且绝对不会是便携式的。谷歌的gperftools有一个例子,你可以看看,但可能比你需要的要复杂得多。

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

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

当我在它时,您的自旋锁实现是有缺陷的。切勿直接在 CAS 上旋转,因为您正在发送垃圾邮件内存屏障。对于任何严重的争用(与雷霆群问题有关(,这将非常缓慢。至少是在 CAS 之前首先检查变量的可用性(如果无障碍读取即可简单(。更好的是不要让所有线程都以相同的值旋转。这应该可以避免关联的缓存行在内核之间乒

乓球。

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

最后,《多处理器编程的艺术》是一本有趣的书,可以更好地熟悉我在这个答案中喷出的所有废话。

我不确定是否有必要在这种情况下锻造自己的 Lock 类。JDK提供了ReentrantLock,它在锁获取期间也利用CAS指令。与您的个人 Lock 类相比,性能应该相当不错。

如果你想

让你的线程在no resource available上犹豫不决,你可以使用SemaphoretryAcquire方法。

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

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

相关内容

  • 没有找到相关文章

最新更新