Кто-нибудь может объяснить интересное поведение спин-блокировки?

Учитывая следующий код

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().

Таким образом, это кажется противоречивым, я ожидал, что случайное распределение ключей приведет к меньшему вращению, чем тот же ключ, но тестирование доказывает обратное.

Кто-нибудь может предложить некоторое понимание этого нелогичного поведения?

Конечно, могут быть лучшие решения для моего дизайна, и я открыт для них, но пока я не могу найти удовлетворительного объяснения тому, что показывают мои тесты, и мое любопытство оставляет меня голодным.

Кроме того, в то время как у теста с одним участником есть более низкий потолок для подсчета вращения, у теста с произвольным элементом есть более низкий потолок для времени вращения, что я и ожидал. Я просто не могу объяснить, почему тест случайных членов обычно дает более высокий потолок для подсчета вращений.

0 ответов

Другие вопросы по тегам