-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Conversation
} | ||
|
||
public sealed override bool Release() | ||
{ | ||
if (IsDisposed) | ||
ThrowHelper.ThrowObjectDisposedException_ArrayMemoryPoolBuffer(); | ||
|
||
int newRefCount = --_refCount; | ||
int newRefCount = Interlocked.Decrement(ref _refCount); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you actually want thread-safety, these Interlocked.Decrement/Increments should be changed to CompareExchange loops, e.g.
// in Retain
while (true)
{
int currentCount = Volatile.Read(ref _refCount);
if (currentCount <= 0) ThrowHelper.ThrowObjectDisposedException_ArrayMemoryPoolBuffer();
if (Interlocked.CompareExchange(ref _refCount, currentCount + 1, currentCount) == currentCount) break;
}
...
// in Release
while (true)
{
int currentCount = Volatile.Read(ref _refCount);
if (currentCount <= 0) ThrowHelper.ThrowObjectDisposedException_ArrayMemoryPoolBuffer();
if (Interlocked.CompareExchange(ref _refCount, currentCount - 1, currentCount) == currentCount)
{
if (currentCount == 1)
{
Dispose();
return true;
}
return false;
}
}
Otherwise, consider this:
- The current _refCount is 1.
- Thread A calls Retain and gets past the IsDisposed check.
- Thread B calls Release, disposing the object, and drops the count to 0.
- Thread A resumes, increments the _refCount, and proceeds to party on the array thinking it successfully owns it.
- Lots of other threads come along, and all Retain/Release, and as the ref count is now positive again, they're all successful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@atsushikan could you look at adding thread safety tests here?
@@ -22,7 +22,9 @@ public sealed override OwnedMemory<T> Rent(int minimumBufferSize = -1) | |||
else if (((uint)minimumBufferSize) > s_maxBufferSize) | |||
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBufferSize); | |||
|
|||
return new ArrayMemoryPoolBuffer(minimumBufferSize); | |||
var buffer = new ArrayMemoryPoolBuffer(minimumBufferSize); | |||
buffer.Retain(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to incur an interlocked operation, as it's not published yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to do this? It changes the contract quite a bit. Can you fix pipelines in the same PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll fix pipelines. It's just strange to get an OwnedMemory with 0 references.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everyone has to agree on the semantics, today our pool in kestrel does not do this so it'll result in memory leaks...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed Retain and added dispose call to Pipelines.
@@ -75,6 +74,7 @@ public void SetMemory(OwnedMemory<byte> ownedMemory, int start, int end, bool re | |||
public void ResetMemory() | |||
{ | |||
_ownedMemory.Release(); | |||
_ownedMemory.Dispose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't correct. We just need to release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure? Pipe is the thing that got OM from the pool, so it is its responsibility to dispose it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But even your PR descriptions says "Releasing ArrayMemoryPool block causes it to be disposed and returned to the pool." Having said that I do think it's a bit strange that we have both Release and Dispose on this type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But calling dispose would also validate that there are no other owners left.
This is beneficial in the case of pipelines. Imagine a situation when someone reads ROS from the pipe, get's memory from it and calls Retain. Now when he tries to advance, Dispose would throw and avoid use after free bugs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the other hand, we can decide to allow consumers to Retain memory received from pipe and calling dispose here would break this feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should just release and not dispose
@@ -22,7 +22,8 @@ public sealed override OwnedMemory<T> Rent(int minimumBufferSize = -1) | |||
else if (((uint)minimumBufferSize) > s_maxBufferSize) | |||
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBufferSize); | |||
|
|||
return new ArrayMemoryPoolBuffer(minimumBufferSize); | |||
var buffer = new ArrayMemoryPoolBuffer(minimumBufferSize); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can revert this piece.
27e7b74
to
4eecbab
Compare
Does disposing throw if the reference count is > 0? |
Yes |
This is a general question about OwnedMemory design: is something creates owned memory for you, should it be returned with ref count equal to one or zero? |
34779af
to
d5467b2
Compare
} | ||
|
||
public sealed override int Length => _array.Length; | ||
|
||
public sealed override bool IsDisposed => _array == null; | ||
|
||
protected sealed override bool IsRetained => _refCount > 0; | ||
protected sealed override bool IsRetained => Volatile.Read(ref _refCount) > 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub, does it need to be interlocked read?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does it need to be interlocked read?
No, it's only 32-bit, so there's no risk of tearing and Interlocked.Read isn't needed. Volatile.Read may not even be necessary depending on what it's used for, but it's unlikely to hurt and so might as well be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I missed that it's an int.
@KrzysztofCwalina why was dispose added to OwnedMemory? What was it's intended semantics? |
394b421
to
efa461f
Compare
Dispose is for "I own the memory; I am disposing it. If you (Memory) still try to access it, you will get an exception as there is a bug somewhere". Relying on just release opens the possibility of undetected leaks. |
@@ -3,6 +3,7 @@ | |||
// See the LICENSE file in the project root for more information. | |||
|
|||
using System.Runtime.InteropServices; | |||
using System.Threading; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: add new line
@@ -3,6 +3,7 @@ | |||
// See the LICENSE file in the project root for more information. | |||
|
|||
using System.Buffers; | |||
using System.Diagnostics; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this using directive?
So when you get memory from the pool, does it mean that you own it and have to dispose? |
The issue with disposing in pipelines is that you lose the ability for the pipe to transfer ownership to another pipe (Append). The original idea was that the reference count would just work because once you transfer the buffer to another pipe, it would just up the reference count and take over those segments. |
4576877
to
edcf2e5
Compare
edcf2e5
to
0843b65
Compare
Okay, I removed dispose call from pipe but kept the part where OwnedMemory is returned with refCount == 1 |
@KrzysztofCwalina speak now or forever hold your peace. This change means that if you get an owned memory from a MemoryPool<T>.Rent, you have to call Release before Dispose. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
* Fix semantics of ArrayMemoryPool * More thread safety * Fix pipes and add tests Commit migrated from dotnet/corefx@9ce9033
https://github.com/dotnet/corefx/issues/27544
https://github.com/dotnet/corefx/issues/27543