编程语言
首页 > 编程语言> > java – 为什么CAS不被认为等同于忙等待循环?

java – 为什么CAS不被认为等同于忙等待循环?

作者:互联网

在过去几天读了一些关于无锁编程的内容,我来到util.java.Random类,使用以下例程创建它的位:

protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
}

根据this SO回答:

So-called lock-free algorithms tend to use tight busy-waiting with a
CAS instruction, but the contention is in ordinary situations so low
that the CPU usually have to iterate only a few times.

wikipedia

Instead of immediately retrying after a CAS operation fails,
researchers have found that total system performance can be improved
in multiprocessor systems—where many threads constantly update some
particular shared variable—if threads that see their CAS fail use
exponential backoff—in other words, wait a little before retrying the
CAS.[4]

可以理解维基百科的文章,它已经被发现但是它还没有被采用,或者CAS指令在失败后人为退避是常见的做法.这是因为这样的循环在cpu使用方面不被认为是危险的还是因为CAS不经常被争议?

第二个问题:是否有任何特定的原因可以创建对种子的引用,或者我们是否也可以简单地使用类范围中的变量?

解决方法:

尝试CAS的多个线程是无锁的(但不是等待的).每当他们都尝试使用相同的旧值时,其中一个线程就会取得进展. https://en.wikipedia.org/wiki/Non-blocking_algorithm.

(多个线程是否都读取相同的旧值或者是否有人看到另一个线程的CAS的结果取决于时间,并且基本上决定了有多少争用.)

这与正常的忙等待循环不同,正常等待一些未知长度的操作,并且如果持有锁的线程被取消调度,则可能无限期地停留.在这种情况下,如果你的CAS无法获得锁定,你肯定想要退出,因为你必须等待另一个线程做一些事情才能成功.

通常,无锁算法用于低争用情况,其中不需要复杂的指数退避.这就是链接的SO答案所说的.

这与Wiki文章中提到的情况有很大不同:许多线程不断更新某些特定的共享变量.这是一个高争用的情况,所以最好让一个线程连续执行一系列更新并使其在L1d缓存中保持热线. (假设您正在使用CAS来实现硬件不直接支持的原子操作,例如原子双精度FP添加,您共享.CAS(旧的,旧的1.0)或其他东西.或者作为无锁的一部分排队等等.)

如果您使用的CAS循环在实践中具有高度竞争性,那么它可能有助于总吞吐量的一些回退,例如在再次尝试之前运行x86暂停指令,以减少在缓存行上敲击的核心.或者对于一个无锁队列,如果你发现它已满或空,那么这基本上是一个等待另一个线程的情况,所以你绝对应该退出.

除x86之外的大多数体系结构都有LL/SC as their atomic RMW primitive,而不是直接硬件CAS.如果其他线程甚至在CAS尝试期间读取缓存行,则从LL / SC构建CAS可能会导致虚假失败,因此可能无法保证至少有一个线程成功.

希望硬件设计人员尝试制造使LL / SC能够抵抗争用的虚假故障的CPU,但我不知道细节.在这种情况下,退避可能有助于避免潜在的活锁.

(在CAS不能因争用而虚假失败的硬件上,livelock是不可能的,例如while(!shared.CAS(old,old<< 1)){}.) Intel’s optimization manual具有等待锁变为空闲的示例,其中它们循环1<< retry_count次数(达到某个最大退避因子)请注意,这不是一个普通的CAS循环,它是无锁算法的一部分;这是为了实现一个锁. 退避正在等待锁变为空闲,而不仅仅是争用访问包含锁本身的缓存行.

  /// Intel's optimization manual
  /// Example 2-2. Contended Locks with Increasing Back-off Example

  /// from section 2.2.4 Pause Latency in Skylake Microarchitecture
  /// (~140 cycles, up from ~10 in Broadwell, thus max backoff should be shorter)
/*******************/
/*Baseline Version */
/*******************/
// atomic {if (lock == free) then change lock state to busy}
while (cmpxchg(lock, free, busy) == fail)
{
   while (lock == busy)
     _mm_pause();
}


/*******************/
/*Improved Version */
/*******************/
int mask = 1;
int const max = 64; //MAX_BACKOFF
while (cmpxchg(lock, free, busy) == fail)
{
   while (lock == busy)
   {
      for (int i=mask; i; --i){
         _mm_pause();
      }
      mask = mask < max ? mask<<1 : max;    // mask <<= 1  up to a max
   }
}

我通常认为在等待锁定时,你想要只读旋转而不是继续尝试使用cmpxchg.我认为英特尔的这个例子只是证明了退避,而不是如何优化锁定以避免延迟解锁线程的其他部分.

无论如何,请记住,这个例子不像我们所说的无锁队列或原子添加或其他原语的CAS重试实现.它正在等待另一个线程释放锁,而不仅仅是在使用读取旧值和尝试使用新值的CAS之间出现的新值时失败.

标签:java,concurrency,lock-free,compare-and-swap
来源: https://codeday.me/bug/20190627/1303612.html