-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Remove reference counters in the concurrent doubly-linked list used in BufferedChannel and Semaphore
#4302
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
Open
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 hidden or 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+movePointersForwardFromRemovedSegmentIn 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
removemethod of the base class is called, which updates the links between neighbouring segments, removing the current segment from the list. Then,movePointersForwardFromRemovedSegmentis called, which moves pointers from the removed segment to the nearest live segment on the right:isLeftmostOrProcessedIn the old implementation, the
cleanPrevmethod 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, theprevlink was cleared.In the new implementation, comparison with counters is incorrect. When
removeis 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_SIZEmay become greater than the counter value. The segment would not be considered the leftmost one, even if bothsendSegmentandreceiveSegmentare already on it.Instead of comparing with counters, the implementation now compares the
idvalues of the pointers:If the
isLeftmostOrProcessedcondition is met, it means all previous segments have been processed. They are no longer needed in the list and should become inaccessible.cleanPrevinvocationsIn the old implementation, a segment was removed only when no pointers referenced it. This ensured that
cleanPrevwas called beforeremoveon the leftmost segment (removecould 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, wherecleanPrevwould be called if necessary. Then, in a new request,moveForwardwould be called, advancing the pointer and triggeringremoveif needed).In the new implementation, there is no guarantee on the order of
cleanPrevandremove. A segment is marked as logically removed ⇒ a pointer skips over it ⇒cleanPrevlocated 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 │ ```cleanPrevwas not invoked on the segment#2, becausereceiveSegmentdid not reach#2, skipping it as alogically removed one. As a result, theprevreference of the leftmost segment was notnullduringvalidate().Solution:
The existing
cleanPrevinvocations in the algorithm's branches were insufficient becauseremovecould bypass these calls and set an already processed segment inthis.next.prev.Instead of adding more calls to
cleanPrevsomewhere in the algorithm, the decision was made to cleanprevreferences insidemoveForward, thus encapsulatingcleanPrevlogic in one place. When a successfulcas(from, to)occurs, the algorithm starts fromto, followsprevreferences and looks for the leftmost segment that no pointers reference. Once found, itsprevreference is cleaned, and themoveForwardcall completes.If at any iteration
prevfor a segment isnull, although the segment did not pass the "no pointers to the left" check, it means a parallelmoveForwardcall cleaned theprevreference.moveForwardFirst 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()#1and#2Second change
Described in the section "`cleanPrev` invocations".moveToSpecifiedOrLast+findSpecifiedOrLastmoveToSpecifiedOrLasthas the same logic asmoveForwardbut inside usesfindSpecifiedOrLast-- a method which returns a segment with the requestedidor the tail in case such segment has not been created yet. In contrast withfindSegmentInternal,findSpecifiedOrLastdoes not add new segments into the segment list.