UdpClient.ReceiveAsync correcte de résiliation anticipée

Bonjour. Je travaille avec UdpClient et j'ai un papier dessus.

pour la lecture j'ai la méthode asynchrone:

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;
}

WithCancellation est ma méthode d'extension pour la résiliation anticipée:

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;
}

et après arrêt manuel de lecture, quand j'appelle Dispose , System.ObjectDisposedException se produit. 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

si j'ai bien compris, racine de l'erreur dans ReceiveAsync dans ma méthode d'arrêt, pour être exact. Mais je ne sais pas comment le résoudre.

Que dois-je faire pour corriger cette erreur?

mise à jour après le commentaire usr:

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;
}

et Dispose invoquant:

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

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

mais le catch ne réagissent pas à ObjectDisposedException .

2
demandé sur EgoPingvina 2016-12-07 17:28:03

2 réponses

et ainsi, après presque une semaine de souffrance, j'ai trouvé la raison et la solution.

d'abord , j'ai regardé le code source UdpClient . La méthode ReceiveAsync :

[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 , j'ai trouvé ce post avec la réponse parfaite: comment interrompre la réception de socket()? , où il est dit:

pour annuler un en attendant l'appel à la méthode BeginConnect (), fermez la Socket. Lorsque la méthode Close () est appelée alors qu'une opération asynchrone est en cours, le rappel fourni à la méthode BeginConnect () est appelé. Un appel ultérieur à la méthode EndConnect(IAsyncResult) lancera une exception ObjectDisposedException pour indiquer que l'opération a été annulée.

et, comme nous pouvons le voir, l'original ReceiveAsync méthode nous rendre le ObjectDisposedException , parce que IOOperation n'a pas été complété après l'invocation de Close .

pour surmonter ce problème j'ai fait comme ceci:

Nouveau ReceiveAsync réalisation:

/// <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);

Nouveau Dispose réalisation:

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

j'espère vraiment que ma décision privera quelqu'un de la douleur que j'ai vécue.

1
répondu EgoPingvina 2017-05-23 11:52:59

la seule façon d'annuler une réception en attente est de déconnecter/arrêter/disposer comme vous l'avez fait. Cela est correct. Vous devez attraper et ignorer cette exception.

c'est un malheureux problème de conception avec le .net Framework que c'est la seule façon de le faire.

Note, que WithCancellation n'annule pas L'IO. La réception est toujours en cours d'exécution. C'est pourquoi WithCancellation doit être suivie par l'élimination de la prise, pour s'assurer qu'il n'y a plus en attente IOs.

0
répondu usr 2016-12-07 14:34:56