【问题标题】:SpinLock scalability and limitationsSpinLock 可扩展性和限制
【发布时间】:2016-09-06 14:44:44
【问题描述】:

我编写了一个简单的程序来测试 CLH 锁的吞吐量。 我有“多核编程的艺术”一书中描述的代码。接下来,我在不断变化的线程数上运行了一个计数器 10 秒,并将 counter/10.0 定义为吞吐量。

我的问题是我得到的结果是否在合乎逻辑的范围内,以及它们的原因可能是什么。我问是因为 CLH 锁的吞吐量下降非常快。 这些是 cLH 锁的结果,其中左侧指定线程计数,右侧是吞吐量(每个线程在受 CLH 锁保护的临界区中递增一次的计数器的大小,除以 10)。

CLH 1 2.89563825E7 2 1.33501436E7 4 5675832.3 8 15868.9 16 11114.4 32 68.4

正如你所见,下车很疯狂,让我觉得我可能把其他事情搞砸了。

这是我的 CLH 锁代码(就像上面提到的书一样):

static class CLHLock implements Lock {
    AtomicReference<QNode> tail;
    ThreadLocal<QNode> myNode, myPred;

    public CLHLock() {
        tail = new AtomicReference<QNode>(new QNode());

        this.myNode = new ThreadLocal<QNode>() {
            protected QNode initialValue() {
                return new QNode();
            }
        };

        this.myPred = new ThreadLocal<QNode>() {
            protected QNode initialValue() {
                return null;
            }
        };
    }

    public void lock() {
        QNode qnode = this.myNode.get();
        qnode.locked.set(true);        

        QNode pred = this.tail.getAndSet(qnode);
        myPred.set(pred);           
        while (pred.locked.get()) {}      
    }

    public void unlock() {
        QNode qnode = this.myNode.get(); 
        qnode.locked.set(false);       
        this.myNode.set(this.myPred.get());   
    }

    static class QNode {  
        public AtomicBoolean locked = new AtomicBoolean(false);
    }
}

运行包括等待 10 秒的主线程,而其他线程尝试锁定、递增,然后解锁,直到一个 volatile 布尔值告诉他们时间到了。

【问题讨论】:

  • 根据我的经验,大多数性能下降是由 CPU 占用过多造成的。 while (pred.locked.get()) {} 可能与while (pred.locked.get()) {Thread.yield();} 更友好。可能没有区别,所以只能评论。

标签: java multithreading locking throughput spinlock


【解决方案1】:

关于您的 CLH 锁实施

实现看起来相当标准,除了繁忙的旋转。让行或停车可能会更好(尽管这需要更多的代码)。

关于您的基准测试结果

从性能测试中判断某些代码的正确性是一项需要至少与从正确性测试中判断某些代码的正确性一样多的知识。

您可能会观察到许多与您的代码没有直接关系的副作用。为了尽量减少这些影响,请使用 JMH 之类的基准测试工具,否则您测量的东西不一定是您的代码。

这是对您的结果的推测性解释,可能不正确,但完全合理:

  • 使用 1 个线程,您的代码执行速度非常快,因为几乎没有锁争用,也没有缓存抖动。您可能会受益于成功的分支预测以及早期启动 JIT 而无需后期去优化。
  • 使用 2 和 4 线程时,吞吐量会有所下降。这还不算太糟糕,因为您仍然有硬件线程,但是现在您遇到了一些缓存抖动(甚至可能是错误共享)、一些一致性流量以及一些分支错误预测(由于您的基准测试的共享基础架构)。尽管并行执行不会增加吞吐量,但您仍然可以。
  • 有了 8 和 16 个线程,您现在已经超出了计算机上可用硬件线程的限制。您开始遇到操作系统调度影响、更严重的缓存抖动以及代码中的严重争用。
  • 使用 32 个线程,您可以超越某些快速硬件缓存机制(L1 缓存、TLB)的限制,并降级到下一个最快的机制。无需超过缓存大小限制即可体验这一点,您也可以超过关联性限制。

【讨论】:

    猜你喜欢
    • 2016-02-18
    • 1970-01-01
    • 2012-05-03
    • 1970-01-01
    • 2018-04-26
    • 1970-01-01
    • 1970-01-01
    • 2011-04-29
    • 2017-01-13
    相关资源
    最近更新 更多