具有取消令牌的NetworkStream.ReadAsync永远不会取消

时间:2012-09-14 09:45:13

标签: .net sockets .net-4.5 async-await cancellation-token

这里有证据 知道这段代码有什么问题吗?

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

7 个答案:

答案 0 :(得分:25)

我终于找到了解决方法。使用Task.WaitAny将异步调用与延迟任务(Task.Delay)组合在一起。当在io任务之前经过延迟时,关闭流。这将迫使任务停止。您应该正确处理io任务上的异步异常。您应该为延迟任务和io任务添加延续任务。

它也适用于tcp连接。在另一个线程中关闭连接(您可以认为它是延迟任务线程)强制所有使用/等待此连接的异步任务停止。

- 编辑 -

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

答案 1 :(得分:16)

取消是合作的。 NetworkStream.ReadAsync必须合作才能取消。它很难做到这一点,因为这可能会使流处于未定义状态。从Windows TCP堆栈中读取了哪些字节,哪些没有? IO不容易取消。

反射器显示NetworkStream未覆盖ReadAsync。这意味着它将获得Stream.ReadAsync的默认行为,它只会抛弃令牌。没有通用的方法可以取消流操作,因此BCL Stream类甚至没有尝试(它无法尝试 - 没有办法做到这一点)。

您应该在Socket上设置超时。

答案 2 :(得分:4)

根据Softlion的答案中的描述:

  

使用Task.WaitAny将异步调用与延迟任务(Task.Delay)结合使用。当在io任务之前经过延迟时,关闭流。这将迫使任务停止。您应该正确处理io任务上的异步异常。你应该为dealy任务和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
    }
}

答案 3 :(得分:3)

出现了一些问题:

  1. CancellationToken抛出OperationCanceledException,而不是TimeoutException(取消并不总是由于超时)。
  2. ReceiveTimeout不适用,因为您正在进行异步读取。即使这样,您也会遇到IOExceptionOperationCanceledException之间的竞争条件。
  3. 由于您正在同步连接套接字,因此您需要在此测试中超时(IIRC,默认连接超时约为90秒,但可以在Windows监视网络速度时更改)。
  4. 测试异步代码的正确方法是使用异步测试:

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

答案 4 :(得分:0)

提供三种不同方法的更多上下文。我的服务监视其他Web应用程序的可用性。因此,它需要建立与各种网站的大量连接。其中一些崩溃/返回错误/变得没有响应。

Y轴-挂起的测试(会话)数。由于部署/重新启动而下降到0。

I。 (1月25日)改造服务后,初始实现将ReadAsync与取消令牌一起使用。这导致大量测试挂起(针对那些网站的运行请求显示,服务器有时确实没有返回内容)。

II。 (2月17日)部署了一个更改,该更改可防止Task.Delay取消。这完全解决了这个问题。

private async Task<int> StreamReadWithCancellationTokenAsync(Stream stream, byte[] buffer, int count, Task cancellationDelayTask)
{
    if (cancellationDelayTask.IsCanceled)
    {
        throw new TaskCanceledException();
    }

    // Stream.ReadAsync doesn't honor cancellation token. It only checks it at the beginning. The actual
    // operation is not guarded. As a result if remote server never responds and connection never closed
    // it will lead to this operation hanging forever.
    Task<int> readBytesTask = stream.ReadAsync(
        buffer,
        0,
        count);
    await Task.WhenAny(readBytesTask, cancellationDelayTask).ConfigureAwait(false);

    // Check whether cancellation task is cancelled (or completed).
    if (cancellationDelayTask.IsCanceled || cancellationDelayTask.IsCompleted)
    {
        throw new TaskCanceledException();
    }

    // Means that main task completed. We use Result directly.
    // If the main task failed the following line will throw an exception and
    // we'll catch it above.
    int readBytes = readBytesTask.Result;

    return readBytes;
}

III(3月3日),在此StackOverflow之后,实现了根据超时关闭流的操作:

using (timeoutToken.Register(() => stream.Close()))
{
    // Stream.ReadAsync doesn't honor cancellation token. It only checks it at the beginning. The actual
    // operation is not guarded. As a result if a remote server never responds and connection never closed
    // it will lead to this operation hanging forever.
    // ReSharper disable once MethodSupportsCancellation
    readBytes = await targetStream.ReadAsync(
        buffer,
        0,
        Math.Min(responseBodyLimitInBytes - totalReadBytes, buffer.Length)).ConfigureAwait(false);
}

此实现带来了挂退(与初始方法的程度不同):

enter image description here

恢复为Task.Delay解决方案。

答案 5 :(得分:0)

单挑,await _stream.WriteAsync(message,cancellationToken);(_ stream是SslStream)在幕后检查取消令牌是否在执行BeginEndWriteAsync之前被取消,因此您必须在开始写入之前先取消令牌。 / p>

public virtual Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        // If cancellation was requested, bail early with an already completed task.
        // Otherwise, return a task that represents the Begin/End methods.
        return cancellationToken.IsCancellationRequested
                    ? Task.FromCanceled(cancellationToken)
                    : BeginEndWriteAsync(buffer, offset, count);
    }

答案 6 :(得分:-2)

我知道它有点晚了,但我通常做的很简单就是取消edit(在我的情况下:NetworkStream)(已测试):

git rebase

}

修改:我已将其放入另一个--no-ff,以澄清。