Make the segment pool lock-free on the JVM#717
Conversation
Egorand
left a comment
There was a problem hiding this comment.
Why not have the new implementation in commonMain? AtomicLong and AtomicReference are available in kotlin-stdlib / kotlin.native.concurrent.
|
|
||
| actual fun take(): Segment { | ||
| return when (val first = firstRef.getAndSet(LOCK)) { | ||
| LOCK -> { |
There was a problem hiding this comment.
I wonder if when will do a ref check or an equality check here? Will this code break if it's an equality check?
There was a problem hiding this comment.
I like your idea of using ===. Lemme switch to that. (They’ll do the same thing regardless, because Segment doesn’t override equals()).
| } | ||
|
|
||
| actual fun recycle(segment: Segment) { | ||
| require(segment.next == null && segment.prev == null) |
There was a problem hiding this comment.
nit: maybe add an error message? Or even split this into two requires?
There was a problem hiding this comment.
It’s more of an assert than anything. This isn’t public API. I’d prefer to not split it up because its primary purpose is documentation.
| if (atomicByteCount.get() >= MAX_SIZE) return // Pool is full. | ||
|
|
||
| val first = firstRef.get() | ||
| if (first == LOCK) return // A take() is currently in progress. |
There was a problem hiding this comment.
Good call. Fixed.
JakeWharton
left a comment
There was a problem hiding this comment.
I would like to see this rolled out to a highly concurrent server as an experiment before we ship it
| /** Total bytes in this pool. */ | ||
| private var atomicByteCount = AtomicLong() | ||
|
|
||
| actual val byteCount: Long |
There was a problem hiding this comment.
What is this used for? The order of operations means that you can observe this being larger than what's actually available.
There was a problem hiding this comment.
Lemme document that.
| if (atomicByteCount.get() >= MAX_SIZE) return // Pool is full. | ||
|
|
||
| val first = firstRef.get() | ||
| if (first == LOCK) return // A take() is currently in progress. |
|
|
||
| actual fun take(): Segment { | ||
| return when (val first = firstRef.getAndSet(LOCK)) { | ||
| LOCK -> { |
There was a problem hiding this comment.
This will do == but we probably want ===
There was a problem hiding this comment.
Yep, good call!
| internal actual object SegmentPool { | ||
| /** The maximum number of bytes to pool. */ | ||
| // TODO: Is 64 KiB a good maximum size? Do we ever have that many idle segments? | ||
| actual val MAX_SIZE = 64 * 1024L // 64 KiB. |
| actual val MAX_SIZE = 64 * 1024L // 64 KiB. | ||
|
|
||
| /** A sentinel segment to indicate that the linked list is currently being modified. */ | ||
| private val LOCK = Segment(ByteArray(0), pos = 0, limit = 0, shared = false, owner = false) |
There was a problem hiding this comment.
Since it’s an object I get that for free.
There was a problem hiding this comment.
Actually, only the fields, not the methods!
There was a problem hiding this comment.
Fixed all to be @JvmStatic. Can’t hurt.
There was a problem hiding this comment.
Why care about @JvmStatic on private fields though? To avoid calling getters internally? Won't it be @JvmField then?
There was a problem hiding this comment.
I decided to go with @JvmStatic on the methods and not on the fields. That’s the least amount of code and the simplest dispatch for callers.
| private val LOCK = Segment(ByteArray(0), pos = 0, limit = 0, shared = false, owner = false) | ||
|
|
||
| /** Singly-linked list of segments. */ | ||
| private var firstRef = AtomicReference<Segment>() |
| private var firstRef = AtomicReference<Segment>() | ||
|
|
||
| /** Total bytes in this pool. */ | ||
| private var atomicByteCount = AtomicLong() |
| /** Total bytes in this pool. */ | ||
| private var atomicByteCount = AtomicLong() | ||
|
|
||
| actual val byteCount: Long |
| actual val byteCount: Long | ||
| get() = atomicByteCount.get() | ||
|
|
||
| actual fun take(): Segment { |
| } | ||
| } | ||
|
|
||
| actual fun recycle(segment: Segment) { |
|
Agreed 100% on wanting to give this lots of exercise before shipping it to everyone! |
884f38f to
52859e5
Compare
|
@Egorand I can’t do |
|
This branch released as |
bnorm
left a comment
There was a problem hiding this comment.
Are aware of https://github.com/Kotlin/kotlinx.atomicfu? Used by kotlinx-coroutines for atomics in common code. Compile time only as well. Could help remove the expect/actual if you wanted to go down that route.
|
|
||
| /** Singly-linked list of segments. */ | ||
| @JvmStatic | ||
| private var firstRef = AtomicReference<Segment>() |
There was a problem hiding this comment.
AtomicReference<Segment?> will get you some null type safety. Otherwise firstRef.getAndSet(LOCK) and firstRef.get() will return platform types.
There was a problem hiding this comment.
Oooh I like it.
Under a high contention environment we'll do more zero-fill and GC instead of locking.
52859e5 to
fae6650
Compare
|
AtomicFU looks brilliant. That’d be an excellent tool for our toolchain if/when we get serious about OkHttp in Kotlin/Native. For now I’m reluctant only because it’s experimental magic, and Okio is no place for experimental magic. |
|
added some notes after reviewing this incredibly late #831 |
| val first = firstRef.get() | ||
| if (first === LOCK) return // A take() is currently in progress. | ||
|
|
||
| segment.next = first |
There was a problem hiding this comment.
sorry if I'm showing my lack of kotlin knowledge, but I'm wondering how these 3 changes will be guaranteed to be visible in the right order to other threads? We aren't using locks or volatile inside Segment as far as I can tell.. what am I missing? (I want to follow up with a comment to explain this later unless it is generally understood kotlin thing)
There was a problem hiding this comment.
ah right. below we are using compareAndSet, not weakCompareAndSet, and that is what's allowing these changes to be safely published to the consumer of the pool (happens-before relationship) https://docs.oracle.com/javase/6/docs/api/java/util/concurrent/atomic/package-summary.html
Under a high contention environment we'll do more zero-fill and GC
instead of locking.