NetworkStream.ReadAsync с маркером отмены никогда не отменяет

Здесь доказательство. Любая идея, что не так в этом коде?

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

Ответ 1

Наконец-то я нашел обходное решение. Объедините асинхронный вызов с задачей задержки (Task.Delay), используя Task.WaitAny. Когда задержка истекает перед задачей io, закройте поток. Это заставит задачу остановиться. Вы должны обработать исключение async в задаче io правильно. И вы должны добавить задачу продолжения как для задачи с задержкой, так и для задачи io.

Он также работает с соединениями tcp. Закрытие соединения в другом потоке (вы можете подумать, что это поток задачи задержки) заставляет все асинхронные задачи использовать/ждать, пока это соединение остановится.

- EDIT -

Другое решение для очистки, предложенное @vtortola: используйте токен отмены, чтобы зарегистрировать вызов в stream.Close:

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

Ответ 2

Отмена сотрудничества. NetworkStream.ReadAsync должен сотрудничать, чтобы быть отмененным. Это сложно сделать, потому что это потенциально может оставить поток в состоянии undefined. Какие байты уже были прочитаны из стека Windows TCP, а что нет? IO нелегко отменяется.

Отражатель показывает, что NetworkStream не отменяет ReadAsync. Это означает, что он получит поведение по умолчанию Stream.ReadAsync, которое просто выбрасывает токен. Существует не общий способ. Операции Stream могут быть отменены, поэтому класс BCL Stream даже не пытается (он не может попробовать - нет способа сделать это).

Вы должны установить таймаут на Socket.

Ответ 3

В описании в Softlion ответ:

Объедините асинхронный вызов с задачей задержки (Task.Delay) с помощью Task.WaitAny. Когда задержка истекает перед задачей io, закройте поток. Это заставит задачу остановиться. Вы должны обработать исключение async в задаче io правильно. И вы должны добавить задачу продолжения как для сложной задачи, так и для задачи io.

Я сделал код, который дает вам асинхронное чтение с таймаутом:

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

Ответ 4

Есть несколько проблем, которые появляются:

  • CancellationToken throws OperationCanceledException, а не TimeoutException (отмена не всегда происходит из-за таймаута).
  • ReceiveTimeout не применяется, так как вы выполняете асинхронное чтение. Даже если бы это произошло, у вас было бы условие гонки между IOException и OperationCanceledException.
  • Поскольку вы синхронно подключаете сокет, вам понадобится высокий тайм-аут в этом тесте (IIRC, таймаут соединения по умолчанию составляет ~ 90 секунд, но его можно изменить, поскольку Windows контролирует скорость сети).
  • Правильный способ тестирования асинхронного кода - с асинхронным тестом:

    [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());
    }
    

Ответ 5

Я знаю это немного позже, но это простая вещь, которую я обычно делаю, чтобы отменить ReadAsync (в моем случае: NetworkStream) (протестировано):



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

EDIT: я поместил это в другую задачу, чтобы очистить.