2016-12-07 15 views
1

Добрый день. Я работаю с UdpClient и накладываю на него обертку.UdpClient.ReceiveAsync исправить раннее завершение

Для чтения у меня есть асинхронный метод:

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 мое разгибание методы досрочного прекращения:

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

И после ручной остановки чтения, когда я называю Dispose, System.ObjectDisposedException происходит. 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 

Если я правильно понял, корень ошибки в ReceiveAsync, в моем методе его остановки, чтобы быть точным. Но я не знаю, как это исправить.

Что я должен сделать, чтобы исправить эту ошибку?

Update после 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; 
} 

и Dispose ссылающееся:

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

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

Но catch не реагируют на ObjectDisposedException.

ответ

0

Итак, после почти недели страданий я нашел причину и решение.

Сначала, я посмотрел исходный код UdpClient. 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); 
} 

На втором, я нашел этот пост с идеальным ответом: How to abort socket's BeginReceive()?, в котором говорилось:

Чтобы отменить отложенный вызов на BeginConnect() метод, закрыть Разъем. Когда метод Close() вызывается во время выполнения асинхронной операции, вызывается обратный вызов, предоставляемый методу BeginConnect(). Последующий вызов метода EndConnect (IAsyncResult) вызовет исключение ObjectDisposedException, чтобы указать, что операция была отменена.

И, как мы видим, оригинальный ReceiveAsync метод возвращает нас ObjectDisposedException, потому что IOOperation не была завершена после Close призыва.

Чтобы преодолеть эту проблему, я сделал так:

Новая ReceiveAsync реализация:

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

Новая Dispose реализация:

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

Я очень надеюсь, что мое решение лишит кого-то еще той боли, которую я испытал.

+1

Это не позволяет вызвать EndReceive во всех случаях, которые необходимы. И если вы вызываете EndReceive, вы снова получите ObjectDisposedException. Но я уже объяснил, как бороться с этим исключением. Исходный код был хорош, за исключением исключенного исключения. – usr

+0

Почему я должен называть 'EndReceive' вручную? Мне это не нужно. И давайте вызывать «Исключение» после этого, поймать его и ничего не делать - это плохая практика. – EgoPingvina

+1

В документации говорится, что он должен быть вызван. Это ошибка использования, чтобы не называть его. Практически это может привести к утечке памяти. Ловить исключение, чтобы справиться с ним, совершенно нормально. Нет ничего подходящего здесь. – usr

0

Единственный способ отменить ожидающий прием - отключить/остановить/удалить, как вы это делали. Это верно. Вам нужно поймать и проигнорировать это исключение.

Проблема с .NET Framework заключается в том, что это единственный способ сделать это.

Обратите внимание, что WithCancellation не отменяет IO. Получатель все еще работает. Вот почему WithCancellation должен следовать за удалением гнезда, чтобы убедиться в отсутствии ожидающих IO.

+0

Но если я вызываю только 'Dispose' (удаляет' .WithCancellation (breakToken) 'в' Receive'), этот объект не будет удаляться. И я не могу поймать 'ObjectDisposedException', я пробовал ... – EgoPingvina

+0

Не уверен, что вы имеете в виду. Код, который у вас есть, в порядке (при условии, что в случае таймаута вы также убиваете сокет). Однако вам нужно проглотить ObjectDisposedException. – usr

+0

try { результат = ожидание client.ReceiveAsync(). WithCancellation (breakToken); } улов (OperationCanceledException) { // Штатная ситуация ручной остановки Task-а } улов (ObjectDisposedException) { } и попробовать { this.client .close(); } улов (ObjectDisposedException) { } не поймать это исключение. – EgoPingvina