44

これが証拠です。
このコードの何が問題なのですか?

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

6 に答える 6

37

私は最終的に回避策を見つけました。Task.WaitAny を使用して、非同期呼び出しを遅延タスク (Task.Delay) と組み合わせます。io タスクの前に遅延が経過したら、ストリームを閉じます。これにより、タスクが強制的に停止されます。io タスクで非同期例外を正しく処理する必要があります。また、delayed タスクと io タスクの両方に継続タスクを追加する必要があります。

また、TCP 接続でも動作します。別のスレッド (遅延タスク スレッドと見なすことができます) で接続を閉じると、この接続を使用/待機しているすべての非同期タスクが強制的に停止されます。

- 編集 -

@vtortola によって提案された別のよりクリーンなソリューション: キャンセル トークンを使用して、stream.Close への呼び出しを登録します。

async ValueTask Read(NetworkStream stream, TimeSpan timeout = default)
{
    if(timeout == default(TimeSpan))
      timeout = TimeSpan.FromSeconds(5);

    using var cts = new CancellationTokenSource(timeout); //C# 8 syntax
    using(cts.Token.Register(() => stream.Close()))
    {
       int receivedCount;
       try
       {
           var buffer = new byte[30000];
           receivedCount = await stream.ReadAsync(buffer, 0, 30000, tcs.Token).ConfigureAwait(false);
       }
       catch (TimeoutException)
       {
           receivedCount = -1;
       }
    }
}
于 2012-10-15T09:43:35.857 に答える
22

キャンセルは協力的です。NetworkStream.ReadAsyncキャンセルできるように協力する必要があります。ストリームが未定義の状態になる可能性があるため、これを行うのは少し難しいです。Windows TCPスタックからすでに読み取られているバイトと、読み取られていないバイトは何ですか?IOは簡単にキャンセルできません。

リフレクターは、をNetworkStreamオーバーライドしないことを示していますReadAsyncStream.ReadAsyncこれは、トークンを破棄するだけのデフォルトの動作を取得することを意味します。ストリーム操作をキャンセルできる一般的な方法はないため、BCLStreamクラスは試行すらしません(試行できません-これを実行する方法はありません)。

にタイムアウトを設定する必要がありSocketます。

于 2012-09-14T20:35:55.743 に答える
6

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
    }
}
于 2014-01-03T18:06:51.617 に答える
3

飛び出すいくつかの問題があります。

  1. CancellationTokenthrows OperationCanceledException、not TimeoutException(キャンセルは常にタイムアウトによるものではありません)。
  2. ReceiveTimeout非同期読み取りを行っているため、適用されません。あったとしても、 と の間IOExceptionで競合状態が発生しOperationCanceledExceptionます。
  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());
    }
    
于 2012-09-14T10:37:45.313 に答える