.net - mvc - NetworkStream.ReadAsync con un token de cancelación nunca cancela
public async task actionresult index() (5)
Finalmente encontré una solución. Combine la llamada asincrónica con una tarea de retardo (Tarea.Delay) usando Task.WaitAny. Cuando la demora transcurra antes de la tarea io, cierre la secuencia. Esto forzará a la tarea a detenerse. Debe manejar la excepción async en la tarea io correctamente. Y debe agregar una tarea de continuación tanto para la tarea retrasada como para la tarea io.
También funciona con conexiones tcp. Cerrar la conexión en otro hilo (podría considerar que es el hilo de la tarea de retardo) fuerza todas las tareas asincrónicas usando / esperando que se detenga esta conexión.
--EDITAR--
Otra solución más limpia sugerida por @vtortola: use el token de cancelación para registrar una llamada a la transmisión. Cerrar:
async Task Read(NetworkStream stream)
{
using (var cancellationTokenSource = new CancellationTokenSource(5000))
{
using(cancellationTokenSource.Token.Register(() => stream.Close()))
{
int receivedCount;
try
{
var buffer = new byte[1000];
receivedCount = await stream.ReadAsync(buffer, 0, 1000, cancellationTokenSource.Token);
}
catch (TimeoutException e)
{
receivedCount = -1;
}
}
}
}
Aquí la prueba.
¿Alguna idea de lo que está mal en este código?
[TestMethod]
public void TestTest()
{
var tcp = new TcpClient() { ReceiveTimeout = 5000, SendTimeout = 20000 };
tcp.Connect(IPAddress.Parse("176.31.100.115"), 25);
bool ok = Read(tcp.GetStream()).Wait(30000);
Assert.IsTrue(ok);
}
async Task Read(NetworkStream stream)
{
using (var cancellationTokenSource = new CancellationTokenSource(5000))
{
int receivedCount;
try
{
var buffer = new byte[1000];
receivedCount = await stream.ReadAsync(buffer, 0, 1000, cancellationTokenSource.Token);
}
catch (TimeoutException e)
{
receivedCount = -1;
}
}
}
Hay algunos problemas que surgen:
-
CancellationToken
arrojaOperationCanceledException
, noTimeoutException
(la cancelación no siempre se debe al tiempo de espera). -
ReceiveTimeout
no se aplica, ya que está haciendo una lectura asincrónica. Incluso si lo hiciera, tendría una condición de carrera entreIOException
yOperationCanceledException
. - Como está conectando de forma síncrona el socket, querrá un alto tiempo de espera en esta prueba (IIRC, el tiempo de espera de conexión predeterminado es ~ 90 segundos, pero se puede cambiar a medida que Windows supervisa las velocidades de red).
La forma correcta de probar el código asíncrono es con una prueba asincrónica:
[TestMethod] public async Task TestTest() { var tcp = new TcpClient() { ReceiveTimeout = 5000, SendTimeout = 20000 }; tcp.Connect(IPAddress.Parse("176.31.100.115"), 25); await Read(tcp.GetStream()); }
La cancelación es cooperativa. NetworkStream.ReadAsync
debe cooperar para poder ser cancelado. Es un poco difícil para hacer eso porque potencialmente dejaría la transmisión en un estado indefinido. ¿Qué bytes ya se han leído de la pila TCP de Windows y qué no? IO no es fácilmente cancelable.
El reflector muestra que NetworkStream
no anula ReadAsync
. Esto significa que obtendrá el comportamiento predeterminado de Stream.ReadAsync
que simplemente arroja el token. No hay forma genérica de que las operaciones de Stream se puedan cancelar, por lo que la clase BCL Stream
ni siquiera lo intenta (no puede intentarlo, no hay forma de hacerlo).
Debe establecer un tiempo de espera en el Socket
.
Sé que es un poco tarde, pero esto es algo simple que suelo hacer para cancelar ReadAsync()
(en mi caso: NetworkStream) (Probado):
Task.Run(() =>
{
// This will create a new CancellationTokenSource, that will cancel itself after 30 seconds
using (CancellationTokenSource TimeOut = new CancellationTokenSource(30*1000))
{
Task<int> r = Stream.ReadAsync(reply, 0, reply.Length);
// This will throw a OperationCanceledException
r.Wait(TimeOut.Token);
}
}
Editar: lo he puesto en otra Task
, para aclarar.
Según la descripción en la respuesta de Softlion:
Combine la llamada asincrónica con una tarea de retardo (Tarea.Delay) usando Task.WaitAny. Cuando la demora transcurra antes de la tarea io, cierre la secuencia. Esto forzará a la tarea a detenerse. Debe manejar la excepción async en la tarea io correctamente. Y debe agregar una tarea de continuación para la tarea dealy y la tarea io.
He creado un código que le da la lectura asíncrona con tiempo de espera:
using System;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace ConsoleApplication2013
{
class Program
{
/// <summary>
/// Does an async read on the supplied NetworkStream and will timeout after the specified milliseconds.
/// </summary>
/// <param name="ns">NetworkStream object on which to do the ReadAsync</param>
/// <param name="s">Socket associated with ns (needed to close to abort the ReadAsync task if the timeout occurs)</param>
/// <param name="timeoutMillis">number of milliseconds to wait for the read to complete before timing out</param>
/// <param name="buffer"> The buffer to write the data into</param>
/// <param name="offset">The byte offset in buffer at which to begin writing data from the stream</param>
/// <param name="amountToRead">The maximum number of bytes to read</param>
/// <returns>
/// a Tuple where Item1 is true if the ReadAsync completed, and false if the timeout occurred,
/// and Item2 is set to the amount of data that was read when Item1 is true
/// </returns>
public static async Task<Tuple<bool, int>> ReadWithTimeoutAsync(NetworkStream ns, Socket s, int timeoutMillis, byte[] buffer, int offset, int amountToRead)
{
Task<int> readTask = ns.ReadAsync(buffer, offset, amountToRead);
Task timeoutTask = Task.Delay(timeoutMillis);
int amountRead = 0;
bool result = await Task.Factory.ContinueWhenAny<bool>(new Task[] { readTask, timeoutTask }, (completedTask) =>
{
if (completedTask == timeoutTask) //the timeout task was the first to complete
{
//close the socket (unless you set ownsSocket parameter to true in the NetworkStream constructor, closing the network stream alone was not enough to cause the readTask to get an exception)
s.Close();
return false; //indicate that a timeout occurred
}
else //the readTask completed
{
amountRead = readTask.Result;
return true;
}
});
return new Tuple<bool, int>(result, amountRead);
}
#region sample usage
static void Main(string[] args)
{
Program p = new Program();
Task.WaitAll(p.RunAsync());
}
public async Task RunAsync()
{
Socket s = new Socket(SocketType.Stream, ProtocolType.Tcp);
Console.WriteLine("Connecting...");
s.Connect("127.0.0.1", 7894); //for a simple server to test the timeout, run "ncat -l 127.0.0.1 7894"
Console.WriteLine("Connected!");
NetworkStream ns = new NetworkStream(s);
byte[] buffer = new byte[1024];
Task<Tuple<bool, int>> readWithTimeoutTask = Program.ReadWithTimeoutAsync(ns, s, 3000, buffer, 0, 1024);
Console.WriteLine("Read task created");
Tuple<bool, int> result = await readWithTimeoutTask;
Console.WriteLine("readWithTimeoutTask is complete!");
Console.WriteLine("Read succeeded without timeout? " + result.Item1 + "; Amount read=" + result.Item2);
}
#endregion
}
}