-
Notifications
You must be signed in to change notification settings - Fork 344
Remove TryGetPointer call from UvTcpConnection #1312
Conversation
@@ -232,15 +233,11 @@ private unsafe Uv.uv_buf_t OnAlloc(UvStreamHandle handle, int status) | |||
var inputBuffer = _input.Writer.Alloc(2048); | |||
|
|||
_inputBuffer = inputBuffer; | |||
|
|||
// REVIEW: Does this leak? | |||
var pinned = inputBuffer.Memory.Pin(); |
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 should call Free in the OnRead callback
@davidfowl Like this? It feels kind of weird having to call |
@@ -6,6 +6,7 @@ | |||
using System.IO; | |||
using System.Threading.Tasks; | |||
using System.IO.Pipelines.Networking.Libuv.Interop; | |||
using System.Buffers; |
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.
Sort usings
Yea, the problem is that OnRead can get called without OnAlloc so you have to know if you did indeed call .Pin so we don't try to double free (decrement the ref count) |
@@ -214,6 +223,8 @@ private static void ReadCallback(UvStreamHandle handle, int status, object state | |||
} | |||
} | |||
} | |||
|
|||
inputBufferHandle.Free(); |
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.
Why is this only freeing in the else case?
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'm only calling Free
in the blocks where _inputBuffer
is null'd.
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 you have to call it everywhere there's a commit
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.
Hmm sorry, do you mind explaining why that's the case? If we commit but keep the buffer then wouldn't it be a problem to drop the handle if it's still going to 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.
Hmm sorry, do you mind explaining why that's the case?
Why would that be a problem? The pin only needs to last as long as the native call that copies data into it. From then the GC is free to move it around. Once the OnRead callback is called it can be freed, otherwise, you may as well keep the TryGetPointer
calls.
If we commit but keep the buffer then wouldn't it be a problem to drop the handle if it's still going to be used?
Why? OnAlloc
would be called again and we would pin the buffer again. The cases where OnRead
gets called before OnAlloc
are pretty rare. The usual pattern is OnAlloc
, OnRead
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.
Ok I think I get it now, I was conflating the lifetime of the handle with the lifetime of the buffer, rather than when it needs to be pinned.
cc: @mjp41 |
@@ -24,6 +22,7 @@ public class UvTcpConnection : IPipeConnection | |||
|
|||
private Task _sendingTask; | |||
private WritableBuffer? _inputBuffer; | |||
private MemoryHandle? _inputBufferPin; |
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.
That you have had to implement this with a nullable type, makes we wonder if we should make Free callable several times, and only the first does something.
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 was going to get around to asking if we should make Free
idempotent. I've explicitly always set it to null
in the unpin method because it's not safe to call multiple times. So that rules out the possibility of that happening
@@ -179,6 +180,7 @@ private static void ReadCallback(UvStreamHandle handle, int status, object state | |||
error = new IOException(uvError.Message, uvError); | |||
|
|||
_inputBuffer?.Commit(); |
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.
Should _inputBuffer be set to null
at this point?
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.
It's possible OnAlloc didn't get called.
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.
So if OnAlloc hasn't been called, won't it already be null
?
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.
That's why we use ?
If your are aiming for parity with gchandle the there should be an "IsAllocated" property |
This allows a user who knows for sure it will be allocated to not have to pay the price of a check. But a user who is unsure can implement the check. And example passing a buffer to an async method unmanaged, you need to pin before but it may return sync in which case you just free and carry on, in the async callback you need to check. |
LGTM, shall we raise an issue to make |
Why would we want to allow multiple calls to free? Isn't it a bug to call it multiple times? If multiple threads call it, it seems like it's left to change that one of the threads won't need the handle after another thread frees it. |
Both I assume GC Handle's |
It is convenient to allow calling Free if the handle was not allocated at all. Considers something like this:
Similar if you do cleanup using try/finally blocks. You either need to structure your try/finally blocks just right, or check for IsAllocated before calling Free. It matches what C/C++ does.
I do not have opinion on this yet. Just listing some reasons why it may be a good idea to allow it… |
If you don't have the lifetime logic totally under control of a single thread, calling IsAllocated does not help either. It has a race and it would just make finding bugs harder. And likewise, I don't have a strong opinion on this. Just trying to explore the pros and cons. |
You might have the life time sorted .. it is a struct right? So consider a gchandle which was the Api guideline rather than having to have nullable you can test in a destructor if it is already freed. If so don't call free. |
@benaadams Hmm, that's interesting, so you can't tie the My feeling is that because this is a public API we should make an effort to avoid footguns like multiple calls to |
You can solve it for a class member or use within a single function stack space/async; its whether to go further and try to solve it for multiple copies? |
Ahh the footgun known as GCHandle... I am missing some toes due to that very thing |
Is this discussion about |
I think we should merge with a change to allow multiple calls to Free, or merge as-is and then change to allow multiple calls. |
If you merge as is. I can fix free today. |
Sure this is fine. I still don't want to remove TryGetPointer as yet. |
Following the discussion in #1298
This is a really simple code change to remove the call to
TryGetPointer
inUvTcpConnection
. It looks like the semantics are similar, but I don't think I'm fully following where that memory comes from and when it gets cleaned up, so want to make sure I'm not missing something important.It probably also has a slight performance penalty.
I don't expect this is correct yet, just a starting point. Once this one's sorted I'll look at the RIO impl.