Home > front end >  Ktor Default pool
Ktor Default pool

Time:10-17

With reference to to Ktor Pool implementation, may someone explain concept behind this implementation of pop and push. I tried to step through the code but I am still not wiser after studying the code.

Below is the code snippet that I am struggling to understand:

   private fun pushTop(index: Int) {
    require(index > 0) { "index should be positive" }
    while (true) { // lock-free loop on top
        val top = this.top // volatile read
        val topVersion = (top shr 32 and 0xffffffffL)   1L 
        val topIndex = (top and 0xffffffffL).toInt() 
        val newTop = topVersion shl 32 or index.toLong() 
        next[index] = topIndex
        if (Top.compareAndSet(this, top, newTop)) return
    }
}

private fun popTop(): Int {
    // lock-free loop on top
    while (true) {
        // volatile read
        val top = this.top
        if (top == 0L) return 0
        val newVersion = (top shr 32 and 0xffffffffL)   1L
        val topIndex = (top and 0xffffffffL).toInt()
        if (topIndex == 0) return 0
        val next = next[topIndex]
        val newTop = newVersion shl 32 or next.toLong()
        if (Top.compareAndSet(this, top, newTop)) return topIndex
    }
}

Can this be written is a simpler way?

CodePudding user response:

There are two things that make this code look a bit unusual. The first is that it's designed to be accessed by multiple threads without using locks. The second is that it's using a single 64-bit value to store two 32-bit integers.

Lock freedom

This looks like some variation on a lock-free stack. It's designed to be accessed by multiple threads at the same time. The rough algorithm works like this:

  • Get the old value and keep a note of it
  • Determine what the new value should be
  • Do an atomic compare-and-set to replace the value if and only if the value still matches the old value we saw at the start
  • If the compare-and-set fails (i.e. somebody else changed the value while we were computing the new value), loop back to the start and try again

Lock-free algorithms like this can be preferable for performance in some types of application. The alternative would be to lock the entire stack so that while one thread is using the stack, all other threads have to wait.

Bit shifting

The other thing that makes this code look more complicated is that it seems to be storing two values in a single variable. The index value passed to pushTop is a 32-bit integer. It then gets combined with a 32-bit incrementing counter, version, before being stored. So top is actually a 64-bit value where the first 32 bits are the 'version' and the last 32 bits are the 'index' that we passed in. Again, this compact storage format is probably a performance optimization.

If we add some comments to the code from pushTop, it looks like this:

val top = this.top // get the current 64-bit value containing 'version' and 'index'
val topVersion = (top shr 32 and 0xffffffffL)   1L  // get the 32 high bits (version) and add 1
val topIndex = (top and 0xffffffffL).toInt() // get the 32 low bits (the old index)
val newTop = topVersion shl 32 or index.toLong() // combine version and new index to a new 64-bit value

You can see much the same thing happening in popTop. It's likely that the version number is included so that the lock-free algorithm can tell the difference between different copies of the same value, if the stack contains duplicates.

  • Related