3

Good day. I work with UdpClient and have wrapper upon it.

For reading I have asynchronous method:

private async Task<byte[]> Receive(UdpClient client, CancellationToken breakToken)
{
    // Выход из async, если произошёл CancellationRequest
    breakToken.ThrowIfCancellationRequested();

    UdpReceiveResult result;
    try
    {
        result = await client.ReceiveAsync().WithCancellation(breakToken);
    }
    catch(OperationCanceledException)
    {
        // Штатная ситуация ручной остановки Task-а
    }

    return result.Buffer;
}

Where WithCancellation is my extension-method for early termination:

public static async Task<T> WithCancellation<T>(
    this Task<T> task, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<bool>();

    using (cancellationToken.Register(
        s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
        tcs))
        if (task != await Task.WhenAny(task, tcs.Task))
            throw new OperationCanceledException(cancellationToken);

    return await task;
}

And after manual reading stop, when I call Dispose, System.ObjectDisposedException occurs. CallStack:

>   System.dll!System.Net.Sockets.UdpClient.EndReceive(System.IAsyncResult asyncResult, ref System.Net.IPEndPoint remoteEP) Unknown
System.dll!System.Net.Sockets.UdpClient.ReceiveAsync.AnonymousMethod__64_1(System.IAsyncResult ar)  Unknown
mscorlib.dll!System.Threading.Tasks.TaskFactory<System.Net.Sockets.UdpReceiveResult>.FromAsyncCoreLogic(System.IAsyncResult iar, System.Func<System.IAsyncResult, System.Net.Sockets.UdpReceiveResult> endFunction, System.Action<System.IAsyncResult> endAction, System.Threading.Tasks.Task<System.Net.Sockets.UdpReceiveResult> promise, bool requiresSynchronization)   Unknown
mscorlib.dll!System.Threading.Tasks.TaskFactory<System.Net.Sockets.UdpReceiveResult>.FromAsyncImpl.AnonymousMethod__0(System.IAsyncResult iar)  Unknown
System.dll!System.Net.LazyAsyncResult.Complete(System.IntPtr userToken) Unknown
System.dll!System.Net.ContextAwareResult.CompleteCallback(object state) Unknown
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state) Unknown
System.dll!System.Net.ContextAwareResult.Complete(System.IntPtr userToken)  Unknown
System.dll!System.Net.LazyAsyncResult.ProtectedInvokeCallback(object result, System.IntPtr userToken)   Unknown
System.dll!System.Net.Sockets.BaseOverlappedAsyncResult.CompletionPortCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* nativeOverlapped)  Unknown
mscorlib.dll!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* pOVERLAP) Unknown

If I understood correctly, root of error in ReceiveAsync, in my method of stop it to be exact. But i don't know how to fix it.

What i must do to correct this error?

Update after usr comment:

private async Task<byte[]> Receive(UdpClient client, CancellationToken breakToken)
{
    // Выход из async, если произошёл CancellationRequest
    breakToken.ThrowIfCancellationRequested();

    UdpReceiveResult result;
    try
    {
        result = await client.ReceiveAsync().WithCancellation(breakToken);
    }
    catch(OperationCanceledException)
    {
        // Штатная ситуация ручной остановки Task-а
    }
    catch(ObjectDisposedException) { }

    return result.Buffer;
}

and Dispose invoking:

public void Dispose()
{
    this.cancelRecieve?.Cancel();
    this.cancelRecieve?.Dispose();

    try
    {
        this.client?.Close();
    }
    catch(ObjectDisposedException) { }
}

But the catch do not react to ObjectDisposedException.

EgoPingvina
  • 734
  • 1
  • 10
  • 33

2 Answers2

5

And so, after nearly a week of suffering, I have found the reason and solution.

At first, I looked at the UdpClient source code. The ReceiveAsync method:

[HostProtection(ExternalThreading = true)]
public Task<UdpReceiveResult> ReceiveAsync()
{
    return Task<UdpReceiveResult>.Factory.FromAsync((callback, state) => BeginReceive(callback, state), (ar)=>
        {
            IPEndPoint remoteEP = null;
            Byte[] buffer = EndReceive(ar, ref remoteEP);
            return new UdpReceiveResult(buffer, remoteEP);

        }, null);
}

At second, I found this post with perfect answer:How to abort socket's BeginReceive()?, in which said:

To cancel a pending call to the BeginConnect() method, close the Socket. When the Close() method is called while an asynchronous operation is in progress, the callback provided to the BeginConnect() method is called. A subsequent call to the EndConnect(IAsyncResult) method will throw an ObjectDisposedException to indicate that the operation has been cancelled.

And, as we can see, the original ReceiveAsync method return us the ObjectDisposedException, because IOOperation has not been completed after Close invoking.

To overcome this problem I have done like this:

New ReceiveAsync realization:

/// <summary>
/// Асинхронный запрос на ожидание приёма данных с возможностью досрочного выхода
/// (для выхода из ожидания вызовите метод Disconnect())
/// </summary>
/// <param name="client">Рабочий экземпляр класса UdpClient</param>
/// <param name="breakToken">Признак досрочного завершения</param>
/// <returns>Если breakToken произошёл до вызова данного метода или в режиме ожидания
/// ответа, вернёт пустой UdpReceiveResult; при удачном получении ответа-результат
/// асинхронной операции чтения</returns>
public Task<UdpReceiveResult> ReceiveAsync(UdpClient client, CancellationToken breakToken)
    => breakToken.IsCancellationRequested
        ? Task<UdpReceiveResult>.Run(() => new UdpReceiveResult())
        : Task<UdpReceiveResult>.Factory.FromAsync(
            (callback, state) => client.BeginReceive(callback, state),
            (ar) =>
                {
                    /// Предотвращение <exception cref="ObjectDisposedException"/>
                    if (breakToken.IsCancellationRequested)
                        return new UdpReceiveResult();

                    IPEndPoint remoteEP = null;
                    var buffer = client.EndReceive(ar, ref remoteEP);
                    return new UdpReceiveResult(buffer, remoteEP);
                },
            null);

New Dispose realization:

protected virtual void Dispose(bool disposing)
{
    if (disposing)
    {
        this.cancelReceive?.Cancel();
        this.client?.Close();
        this.cancelReceive?.Dispose();
    }
}

I very much hope, that my decision will deprive someone else of the pain that I experienced.

Community
  • 1
  • 1
EgoPingvina
  • 734
  • 1
  • 10
  • 33
  • 1
    This fails to call EndReceive in all cases which is required. And if you make it call EndReceive you will again receive the ObjectDisposedException. But I already explained how to deal with that exception. Your original code was fine except for the broken exception. – usr Dec 08 '16 at 14:26
  • Why I must call `EndReceive` manually? I do not need it. And give to be invoked `Exception` after that catch it and do nothing-is bad practice. – EgoPingvina Dec 08 '16 at 14:29
  • 1
    The documentation says it must be called. It's a usage error to not call it. Practically, it might leak memory. Catching an exception to handle it is totally fine. Doing nothing is appropriate here. – usr Dec 08 '16 at 14:32
  • If you open sorce code you can see, that `UdpClient.Close` invoke `Socket.InternalShutdown` and `Socket.Close`, in which invoking handler's release and `this(socket).Dispose`. You can see it here: https://referencesource.microsoft.com/#System/net/System/Net/Sockets/UDPClient.cs,c84007a498b38b5f and https://referencesource.microsoft.com/#System/net/System/Net/Sockets/Socket.cs,c84007a498b38b5f. – EgoPingvina Dec 08 '16 at 14:40
  • 1
    That's not guaranteed. Also it does not seem to show that there are no leaks. Also, there is no reason to do any of this. The new ReceiveAsync code is more complicate. All you need is the catch(ODE). – usr Dec 08 '16 at 14:53
  • If you think, what Microsoft's release methods realization not work correct, send mail for them. I test my case by JetBrains dotMemory with hundred of create/dispose UdpDevice(my wrapper upon UdpClient) and it work without leaks. – EgoPingvina Dec 08 '16 at 15:06
  • 1
    The Framework is correct. You're misusing it according to the documentation. – usr Dec 08 '16 at 15:12
  • here https://msdn.microsoft.com/en-us/library/dxkwh6zw(v=vs.110).aspx written: "To cancel a pending BeginReceive, call the Close method.". And here said that in case with `UdpClient` it correct: http://stackoverflow.com/questions/30222269/is-it-safe-to-call-beginxxx-without-calling-endxxx-if-the-instance-is-already-di. still here: https://github.com/dotnet/corefx/issues/14308#issuecomment-265776177. – EgoPingvina Dec 08 '16 at 16:19
  • 2
    Closing is correct but you must always call the End method. The answer is wrong for the same reason that I already explained: It's not guaranteed to be this way. The answer relies on decompiling the *current* code. Might break at any time. The docs state `The asynchronous BeginReceive operation must be completed by calling the EndReceive method`. – usr Dec 08 '16 at 16:24
  • If the Send results in no response being received, both ReceiveAsync() and EndReceive() will hang the thread on which they're called forever. The advice to naively call ReceiveAsync() will eventually end up with hung threads, as every network connection eventually flakes out, endpoints go down, etc. – Chris Moschini Oct 16 '18 at 22:15
0

The only way to cancel a pending receive is to disconnect/stop/dispose as you did. This is correct. You need to catch and ignore that exception.

It is an unfortunate design problem with the .NET Framework that this is the only way to do it.

Note, that WithCancellation does not cancel the IO. The Receive is still running. That's why WithCancellation must be followed by disposing of the socket to make sure that there are no further pending IOs.

usr
  • 168,620
  • 35
  • 240
  • 369
  • But if I call only `Dispose`(delete `.WithCancellation(breakToken)` in `Receive`), this object will not disposing. And I can not catch `ObjectDisposedException`, I have tried... – EgoPingvina Dec 07 '16 at 14:38
  • Not sure what you mean. The code that you have is fine (assuming that in case of timeout you also kill the socket). You need to swallow the ObjectDisposedException, though. – usr Dec 07 '16 at 14:39
  • try { result = await client.ReceiveAsync().WithCancellation(breakToken); } catch(OperationCanceledException) { // Штатная ситуация ручной остановки Task-а } catch(ObjectDisposedException) { } and try { this.client?.Close(); } catch(ObjectDisposedException) { } are not catch this exception. – EgoPingvina Dec 07 '16 at 14:44
  • Are you signaling the breakToken? In that case the Receive task is thrown away. The Receive task is the task that becomes faulted with ObjeDispEx. – usr Dec 07 '16 at 15:23
  • Yes, but forgot wtire it here) – EgoPingvina Dec 07 '16 at 15:26
  • `catch` do not react to `ObjectDisposedException` – EgoPingvina Dec 07 '16 at 15:28
  • I just told you why: `In that case the Receive task is thrown away. The Receive task is the task that becomes faulted with ObjeDispEx`. What about that? – usr Dec 07 '16 at 15:47
  • Is there a code example anywhere of the final, proper solution that handles network/endpoint down? Also, if .Close() then .EndReceive() is the correct approach, it seems the only way to proceed is to constantly spin up UdpClients, since a flaky net connection could cause .EndReceive() to hang intermittently, forcing this workaround to constantly close them, meaning every UdpClient is safely usable exactly once. – Chris Moschini Oct 16 '18 at 22:19
  • @ChrisMoschini for UPD there's never a reason to cancel during normal operations. The idea is to keep one outstanding receive call at any time and process the results that come in. If the network is temporarily down nothing will come in. If it goes up it will start to come in. Cancel only (by closing) if you want to shut down listening. – usr Oct 17 '18 at 14:05
  • Well you can't do that with .Receive() - that straight up hangs the thread. In a simple .Send() (endpoint replies) .Receive() scenario on a flaky network connection, you're going to hang thread after thread after thread on each reply the network eats. – Chris Moschini Oct 17 '18 at 16:28
  • @ChrisMoschini there should be only one thread reading from the UDP socket and doing nothing else. It's normal that this thread will be blocked most of the time. – usr Oct 17 '18 at 20:29
  • I see, so if multiple threads are sending and anticipating replies, you'd basically need to put that thread out (maybe ReceiveAsync uses IO ports to reduce the damage done by a constantly blocking thread?) pushing anything it receives to, maybe, a message box, and the senders anticipating responses would I guess open up to any responses received until the timeout expires? Seems like that's certain to leave you unsure which send pairs with which receive? The particular endpoint I'm using does at least use a SeqNum in each send/receive so I can pair it after I suppose. – Chris Moschini Oct 17 '18 at 20:49
  • @ChrisMoschini if you had multiple outstanding receives then the replys would be delivered randomly. So in any case you need a mechanism to route the incoming packets to the code that needs them. It's easiest to have one thread do that. You can use async IO to save that thread but a single thread is very cheap. You can make the receive code look easier by making it look like `await myUdpReceiver.ReceiveFromAsync(someMessageID)`. Then receiver would then keep a `Dictionary>` and complete the corresponding task when a packet arrives. – usr Oct 19 '18 at 08:43