Кто-нибудь может объяснить интересное поведение спин-блокировки?
Учитывая следующий код
case class Score(value: BigInt, random: Long = randomLong) extends Comparable[Score] {
override def compareTo(that: Score): Int = {
if (this.value < that.value) -1
else if (this.value > that.value) 1
else if (this.random < that.random) -1
else if (this.random > that.random) 1
else 0
}
override def equals(obj: _root_.scala.Any): Boolean = {
val that = obj.asInstanceOf[Score]
this.value == that.value && this.random == that.random
}
}
@tailrec
private def update(mode: UpdateMode, member: String, newScore: Score, spinCount: Int, spinStart: Long): Unit = {
// Caution: there is some subtle logic below, so don't modify it unless you grok it
try {
Metrics.checkSpinCount(member, spinCount)
} catch {
case cause: ConcurrentModificationException =>
throw new ConcurrentModificationException(Leaderboard.maximumSpinCountExceeded.format("update", member), cause)
}
// Set the spin-lock
put(member, None) match {
case None =>
// BEGIN CRITICAL SECTION
// Member's first time on the board
if (scoreToMember.put(newScore, member) != null) {
val message = s"$member: added new member in memberToScore, but found old member in scoreToMember"
logger.error(message)
throw new ConcurrentModificationException(message)
}
memberToScore.put(member, Some(newScore)) // remove the spin-lock
// END CRITICAL SECTION
case Some(option) => option match {
case None => // Update in progress, so spin until complete
//logger.debug(s"update: $member locked, spinCount = $spinCount")
for (i <- -1 to spinCount * 2) {Thread.`yield`()} // dampen contention
update(mode, member, newScore, spinCount + 1, spinStart)
case Some(oldScore) =>
// BEGIN CRITICAL SECTION
// Member already on the leaderboard
if (scoreToMember.remove(oldScore) == null) {
val message = s"$member: oldScore not found in scoreToMember, concurrency defect"
logger.error(message)
throw new ConcurrentModificationException(message)
} else {
val score =
mode match {
case Replace =>
//logger.debug(s"$member: newScore = $newScore")
newScore
case Increment =>
//logger.debug(s"$member: newScore = $newScore, oldScore = $oldScore")
Score(newScore.value + oldScore.value)
}
//logger.debug(s"$member: updated score = $score")
scoreToMember.put(score, member)
memberToScore.put(member, Some(score)) // remove the spin-lock
//logger.debug(s"update: $member unlocked")
}
// END CRITICAL SECTION
// Do this outside the critical section to reduce time under lock
if (spinCount > 0) Metrics.checkSpinTime(System.nanoTime() - spinStart)
}
}
}
Есть две важные структуры данных: memberToScore и ScoreToMember. Я экспериментировал с использованием обоих TrieMap[String,Option[Score]]
а также ConcurrentHashMap[String,Option[Score]]
для memberToScore и оба ведут себя одинаково.
Пока что мое тестирование показывает, что код верен и безопасен для потоков, но загадкой является производительность спин-блокировки. В системе с 12 аппаратными потоками и 1000 итерациями по 12 фьючерсам: попадание в один и тот же элемент все время приводит к циклам вращения 50 или более, но попадание в случайное распределение членов может привести к циклам вращения 100 или более. Поведение ухудшается, если я не ослабляю вращение, не повторяя вызовы yield().
Таким образом, это кажется противоречивым, я ожидал, что случайное распределение ключей приведет к меньшему вращению, чем тот же ключ, но тестирование доказывает обратное.
Кто-нибудь может предложить некоторое понимание этого нелогичного поведения?
Конечно, могут быть лучшие решения для моего дизайна, и я открыт для них, но пока я не могу найти удовлетворительного объяснения тому, что показывают мои тесты, и мое любопытство оставляет меня голодным.
Кроме того, в то время как у теста с одним участником есть более низкий потолок для подсчета вращения, у теста с произвольным элементом есть более низкий потолок для времени вращения, что я и ожидал. Я просто не могу объяснить, почему тест случайных членов обычно дает более высокий потолок для подсчета вращений.