嵌套自旋锁与易失性检查
我正要写一些关于这个的东西,但也许在显得像个傻瓜之前最好先有第二个意见......
所以下一段代码(android的房间包v2.4.1,RoomTrackingLiveData)的想法是,获胜者线程保持活动状态,并被迫检查计算时可能已进入进程的争用(来自丢失线程)。 虽然这些丢失线程执行的失败 CAS 操作使它们无法输入和执行代码,从而防止重复信号(mComputeFunction.call() 或 postValue())。
final Runnable mRefreshRunnable = new Runnable() {
@WorkerThread
@Override
public void run() {
if (mRegisteredObserver.compareAndSet(false, true)) {
mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
}
boolean computed;
do {
computed = false;
if (mComputing.compareAndSet(false, true)) {
try {
T value = null;
while (mInvalid.compareAndSet(true, false)) {
computed = true;
try {
value = mComputeFunction.call();
} catch (Exception e) {
throw new RuntimeException("Exception while computing database"
+ " live data.", e);
}
}
if (computed) {
postValue(value);
}
} finally {
mComputing.set(false);
}
}
} while (computed && mInvalid.get());
}
};
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread
@Override
public void run() {
boolean isActive = hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
getQueryExecutor().execute(mRefreshRunnable);
}
}
}
};
这里最明显的事情是原子被用于它们不擅长的一切:
识别失败者并忽略胜利者(反应模式需要什么)。
AND 发生一次行为,由失败者线程执行。
因此,这与原子能够实现的目标完全相反,因为它们非常擅长定义获胜者,并且任何需要“发生一次”的事情都无法确保状态一致性(最后一个适合开始一场关于并发性,我肯定会同意任何结论)。
如果原子用作:“争用检查器”和“争用阻止器”,那么我们可以在成功 CAS 后通过对原子引用进行易失性检查来实现确切的原理。
并在该过程的每个其他步骤中根据快照/见证检查此易失性。
private final AtomicInteger invalidationCount = new AtomicInteger();
private final IntFunction<Runnable> invalidationRunnableFun = invalidationVersion -> (Runnable) () -> {
if (invalidationVersion != invalidationCount.get()) return;
try {
T value = computeFunction.call();
if (invalidationVersion != invalidationCount.get()) return; //In case computation takes too long...
postValue(value);
} catch (Exception e) {
e.printStackTrace();
}
};
getQueryExecutor().execute(invalidationRunnableFun.apply(invalidationCount.incrementAndGet()));
在这种情况下,每个线程都有责任检查自己在竞争通道中的位置,如果它们的位置移动并且不再位于前面,则意味着有新线程进入了进程,它们应该停止进一步处理。
这个替代方案是如此简单得可笑,以至于我的第一个问题是:
他们为什么不这样做?
也许我的解决方案有一个缺陷...但是第一个替代方案(嵌套自旋锁)的问题是它遵循原子 CAS 操作无法再次验证的想法,并且验证只能通过cmpxchg 进程.... 这是...错误的。
它还遵循一种常见的(但错误的)信念,即在成功的 CAS 后定义的内容是上帝的圣言……因为我看到代码一旦进入 if 体就很少检查并发问题。
if (mInvalid.compareAndSet(false, true)) {
// Ummm... yes... mInvalid is still true...
// Let's use a second atomicReference just in case...
}
它还遵循涉及“双<输入某事>”的常见代码约定。在并发场景下。
因此,仅仅因为第一个代码遵循了这些想法,我才倾向于相信我的解决方案是一个有效且更好的选择。
尽管有一个支持“嵌套自旋锁”选项的论点,但并没有多大意义:
第一个替代方案“更安全”,正是因为它速度较慢,因此它有更多的时间来识别结束时的争用。当前传入线程。
但甚至不是 100% 安全,因为“发生一次”的事情是无法保证的。
代码还有一种行为,即当它到达传入线程连续流的末尾时,将依次分派 2 个信号,倒数第二个信号,然后是最后一个信号一。
但是,如果它因为速度较慢而更安全,那么这是否会违背使用原子的目标,因为它们的使用首先应该是为了成为更好的性能替代方案?
I was about to write something about this, but maybe it is better to have a second opinion before appearing like a fool...
So the idea in the next piece of code (android's room package v2.4.1, RoomTrackingLiveData), is that the winner thread is kept alive, and is forced to check for contention that may have entered the process (coming from losing threads) while computing.
While fail CAS operations performed by these losing threads keep them out from entering and executing code, preventing repeating signals (mComputeFunction.call() OR postValue()).
final Runnable mRefreshRunnable = new Runnable() {
@WorkerThread
@Override
public void run() {
if (mRegisteredObserver.compareAndSet(false, true)) {
mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
}
boolean computed;
do {
computed = false;
if (mComputing.compareAndSet(false, true)) {
try {
T value = null;
while (mInvalid.compareAndSet(true, false)) {
computed = true;
try {
value = mComputeFunction.call();
} catch (Exception e) {
throw new RuntimeException("Exception while computing database"
+ " live data.", e);
}
}
if (computed) {
postValue(value);
}
} finally {
mComputing.set(false);
}
}
} while (computed && mInvalid.get());
}
};
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread
@Override
public void run() {
boolean isActive = hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
getQueryExecutor().execute(mRefreshRunnable);
}
}
}
};
The most obvious thing here is that atomics are being used for everything they are not good at:
Identifying losers and ignoring winners (what reactive patterns need).
AND a happens once behavior, performed by the loser thread.
So this is completely counter intuitive to what atomics are able to achieve, since they are extremely good at defining winners, AND anything that requires a "happens once" becomes impossible to ensure state consistency (the last one is suitable to start a philosophical debate about concurrency, and I will definitely agree with any conclusion).
If atomics are used as: "Contention checkers" and "Contention blockers" then we can implement the exact principle with a volatile check of an atomic reference after a successful CAS.
And checking this volatile against the snapshot/witness during every other step of the process.
private final AtomicInteger invalidationCount = new AtomicInteger();
private final IntFunction<Runnable> invalidationRunnableFun = invalidationVersion -> (Runnable) () -> {
if (invalidationVersion != invalidationCount.get()) return;
try {
T value = computeFunction.call();
if (invalidationVersion != invalidationCount.get()) return; //In case computation takes too long...
postValue(value);
} catch (Exception e) {
e.printStackTrace();
}
};
getQueryExecutor().execute(invalidationRunnableFun.apply(invalidationCount.incrementAndGet()));
In this case, each thread is left with the individual responsibility of checking their position in the contention lane, if their position moved and is not at the front anymore, it means that a new thread entered the process, and they should stop further processing.
This alternative is so laughably simple that my first question is:
Why didn't they do it like this?
Maybe my solution has a flaw... but the thing about the first alternative (the nested spin-lock) is that it follows the idea that an atomic CAS operation cannot be verified a second time, and that a verification can only be achieved with a cmpxchg process.... which is... false.
It also follows the common (but wrong) believe that what you define after a successful CAS is the sacred word of GOD... as I've seen code seldom check for concurrency issues once they enter the if body.
if (mInvalid.compareAndSet(false, true)) {
// Ummm... yes... mInvalid is still true...
// Let's use a second atomicReference just in case...
}
It also follows common code conventions that involve "double-<enter something>" in concurrency scenarios.
So only because the first code follows those ideas, is that I am inclined to believe that my solution is a valid and better alternative.
Even though there is an argument in favor of the "nested spin-lock" option, but does not hold up much:
The first alternative is "safer" precisely because it is SLOWER, so it has MORE time to identify contention at the end of the current of incoming threads.
BUT is not even 100% safe because of the "happens once" thing that is impossible to ensure.
There is also a behavior with the code, that, when it reaches the end of a continuos flow of incoming threads, 2 signals are dispatched one after the other, the second to last one, and then the last one.
But IF it is safer because it is slower, wouldn't that defeat the goal of using atomics, since their usage is supposed to be with the aim of being a better performance alternative in the first place?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论