-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove reference counters in the concurrent doubly-linked list used in BufferedChannel
and Semaphore
#4302
Draft
de-shyt
wants to merge
15
commits into
Kotlin:develop
Choose a base branch
from
de-shyt:channels-remove-counters
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
04522a4
to
7a9906b
Compare
ndkoval
reviewed
Jan 29, 2025
ndkoval
reviewed
Jan 29, 2025
ndkoval
reviewed
Jan 29, 2025
ndkoval
reviewed
Jan 29, 2025
ndkoval
reviewed
Jan 29, 2025
ndkoval
reviewed
Jan 29, 2025
ndkoval
reviewed
Jan 29, 2025
BufferedChannel
and Semaphore
ndkoval
reviewed
Jan 30, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Old Implementation
In the old implementation counters were used to track how many pointers reference each segment. When a pointer moves to the next live segment, the counter of the current segment decreases, and the counter of the next segment increases.
A segment becomes logically removed under two conditions:
Physical removal happens after all pointers referencing the segment have moved forward.
New Implementation
In the new implementation counters are no longer used. The logical removal now depends on only one condition — all cells are in the interrupted state.
When physical removal occurs, pointers referencing the removed segment are subject to being moved to the next live segment (this is always possible because the tail of the list is never marked as removed).
Methods
remove
+movePointersForwardFromRemovedSegment
In the old implementation, all pointers moved to another segment first, and only then could the segment become logically removed.
In the new implementation, removal does not depend on the position of pointers: a segment can be physically removed while pointers remain on it. They need to be manually moved to prevent memory leaks.
First, the
remove
method of the base class is called, which updates the links between neighbouring segments, removing the current segment from the list. Then,movePointersForwardFromRemovedSegment
is called, which moves pointers from the removed segment to the nearest live segment on the right:isLeftmostOrProcessed
In the old implementation, the
cleanPrev
method was triggered based on the conditionsegment.id * SEGMENT_SIZE < channel.sendersCounter
(orsegment.id * SEGMENT_SIZE < channel.receiversCounter
) within the request. If this condition was met, theprev
link was cleared.In the new implementation, comparison with counters is incorrect. When
remove
is called, pointers move from the removed segment to the nearest live segment on the right (regardless of which cell contains the last request). As a result, on the new segment, the valueid * SEGMENT_SIZE
may become greater than the counter value. The segment would not be considered the leftmost one, even if bothsendSegment
andreceiveSegment
are already on it.Instead of comparing with counters, the implementation now compares the
id
values of the pointers:If the
isLeftmostOrProcessed
condition is met, it means all previous segments have been processed. They are no longer needed in the list and should become inaccessible.cleanPrev
invocationsIn the old implementation, a segment was removed only when no pointers referenced it. This ensured that
cleanPrev
was called beforeremove
on the leftmost segment (remove
could not be called first. If it was the leftmost segment, then a pointer was still referencing it. The algorithm would first reach the end of the branch, wherecleanPrev
would be called if necessary. Then, in a new request,moveForward
would be called, advancing the pointer and triggeringremove
if needed).In the new implementation, there is no guarantee on the order of
cleanPrev
andremove
. A segment is marked as logically removed ⇒ a pointer skips over it ⇒cleanPrev
located in the request branch is not called. For example, the following failed scenario occurs:Full stacktrace
``` │ Thread 1 │ Thread 2 │ send(2): │ s=0, id=0 │ segm=#1 │ state: null->buffered │ │ send(2): suspend + cancel │ s=1, id=1 │ segm=findSegmentSend(1,#1): #2 │ #1.findSegmentInternal(id=1): #2 │ cur=#1 │ #1.id<1: true, #1.next: null │ #1.trySetNext(#2): true, #1.isRemoved: false│ cur=#2 │ #2.id<1: false, #2.isRemoved: false │ return #2 │ S.cas(#1,#2): true │ return #2 │ state: null->Coroutine │ Coroutine cancelled │ │ receive(): 2 │ r=0, id=0 │ segm=#1 │ state: buffered->done_rcv │ expandBuffer(): │ b=1, s=2, s<=b: false │ segm=findSegmentBufferEnd(1,#1): #2 │ #1.findSegmentInternal(id=1): #2 │ cur=#1 │ #1.id<1: true, cur=#2 │ #2.id<1: false, #2.isRemoved: false │ return #2 │ EB.cas(#1,#2): true │ return #2 │ state: Coroutine->resuming_eb │ tryResume(): false │ state: resuming_eb->int_send │ #2.onCancelledRequest(): │ cleanedSlots.incAndGet() │ isRemoved: true │ remove(): │ │ receive(): suspend │ r=1, id=1 │ segm=findSegmentReceive(1,#1): null │ #1.findSegmentInternal(id=1): #3 │ cur=#1 │ #1.id<1: true, cur=#2 │ #2.id<1: false, #2.isRemoved: true, #2.next: null │ #2.trySetNext(#3): true, #2.isRemoved: true │ #2.remove(): │ prev=#1, next=#3 │ #3.prev=#1 │ #1.next=#3 │ cur=#3 │ #3.id<1: false, #3.isRemoved: false │ return #3 │ R.cas(#1,#3): true │ return null │ r=2, id=2 │ segm=#3 │ state: null->Coroutine │ expandBuffer(): │ b=2, s=2, s<=b: true │ EB.moveToSpecifiedOrLast(2,#2): │ #2.findSpecifiedOrLast(id=2): #3 │ cur=#2 │ #2.id<2: true, cur=#3 │ #3.id<2: false, break │ return #3 │ EB.cas(#2,#3): true prev=#1, next=#3 │ #3.next=#1 │ #1.next=#3 │ b=3, s=2, s<=b: true │ segm=#3, #3.id<3: true │ BE.moveToSpecifiedOrLast(3,#3): │ #3.findSpecifiedOrLast(id=3): #3 │ cur=#3 │ #3.id<3: true, #3.next: null, break │ return #3 │ EB.cas(#3, #3): true │ #1.cleanPrev() │ return 2 │ ```cleanPrev
was not invoked on the segment#2
, becausereceiveSegment
did not reach#2
, skipping it as alogically removed one. As a result, theprev
reference of the leftmost segment was notnull
duringvalidate()
.Solution:
The existing
cleanPrev
invocations in the algorithm's branches were insufficient becauseremove
could bypass these calls and set an already processed segment inthis.next.prev
.Instead of adding more calls to
cleanPrev
somewhere in the algorithm, the decision was made to cleanprev
references insidemoveForward
, thus encapsulatingcleanPrev
logic in one place. When a successfulcas(from, to)
occurs, the algorithm starts fromto
, followsprev
references and looks for the leftmost segment that no pointers reference. Once found, itsprev
reference is cleaned, and themoveForward
call completes.If at any iteration
prev
for a segment isnull
, although the segment did not pass the "no pointers to the left" check, it means a parallelmoveForward
call cleaned theprev
reference.moveForward
First change
The pointer may be moved to a segment that has already been physically deleted, resulting in a memory leak. Example of a failed scenario:Final state:
validate()
#1
and#2
Second change
Described in the section "`cleanPrev` invocations".moveToSpecifiedOrLast
+findSpecifiedOrLast
moveToSpecifiedOrLast
has the same logic asmoveForward
but inside usesfindSpecifiedOrLast
-- a method which returns a segment with the requestedid
or the tail in case such segment has not been created yet. In contrast withfindSegmentInternal
,findSpecifiedOrLast
does not add new segments into the segment list.