我编写了一个简单的程序来测试CLH锁的吞吐量。
我拥有“多核编程的艺术”一书中描述的代码。接下来,我在不断变化的线程数上运行一个计数器10秒钟,并将counter / 10.0定义为吞吐量。
我的问题是,我获得的结果是否在逻辑范围内,以及它们可能保持现状的原因可能是什么?我问是因为CLH锁的吞吐量下降非常快。
这些是cLH锁的结果,其中左侧指定线程计数,右侧为吞吐量(在CLH锁保护的临界区中,每个线程将其递增一次的计数器的大小除以10)。CLH1 2.89563825E72 1.33501436E74 5675832.38 15868.916 11114.432 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秒组成,而其他线程则尝试锁定,递增然后解锁,直到一个不稳定的布尔值告诉他们时间到了。
最佳答案
关于您的CLH锁实施
除了忙碌的旋转外,该实现看起来相当标准。您可能最好放弃或停车(尽管这将需要更多的代码)。
关于基准测试结果
从性能测试中判断某些代码的正确性是一项任务,至少需要与从其正确性测试中判断某些代码的正确性一样多的知识。
您可能正在观察与代码没有直接关系的许多副作用。为了最大程度地减少这些影响,请使用JMH之类的基准测试工具,否则,您正在测量的不一定是您的代码。
这是关于您的结果的推测性解释,可能不正确,但完全合理:
使用1个线程,您的代码执行速度非常快,因为锁上几乎没有争用,也没有缓存溢出。您可能会受益于成功的分支预测和早期的JIT启动,而无需随后进行非优化。
使用2和4个线程,吞吐量会有所下降。还算不错,因为您仍然具有硬件线程,但是现在您遇到了一些缓存抖动(甚至可能是错误共享),一些一致性流量以及某些分支预测错误(由于基准测试的共享基础结构)。尽管并行执行不会增加吞吐量,但是您仍然可以。
使用8和16线程,您现在已超出计算机上可用硬件线程的限制。您开始体验操作系统调度的效果,更重要的缓存抖动以及代码中的重大争用。
使用32个线程,您可以超出某些快速硬件缓存机制(L1缓存,TLB)的限制,然后降级到下一个最快的机制。不必超过缓存大小限制即可体验到这一点,您也可以超过关联性限制。
关于java - SpinLock的可扩展性和局限性,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/37158564/