这种方法比在Task.Run?

时间:2017-08-02 14:55:45

标签: c# async-await networkstream

编辑:根据discussionStephen Cleary,我最终没有采用这种方法。如果您对我的方式不同感兴趣,请查看下面的answer

我正在寻找一种通过超时从NetworkStream异步读取的方法。当然,问题是无法取消ReadAsync() NetworkStream,因为它只会忽略CancellationToken。我读了一个答案,建议在令牌取消时关闭流,但在我的情况下,这不是一个选项,因为Tcp连接必须保持打开状态。所以我提出了以下代码,但我想知道这是否比做

更好
Task.Run(() => stream.Read(buffer, offset, count)

并阻止线程。

public static class TcpStreamExtension
{
    public static async Task<int> ReadAsyncWithTimeout(this NetworkStream stream, byte[] buffer, int offset, int count)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        bool keepTrying = true;
        Timer timer = new Timer(stream.ReadTimeout);
        timer.Elapsed += new ElapsedEventHandler((sender, args) => stopTrying(sender, args, cts, out keepTrying));
        timer.Start();

        try
        {
            if (stream.CanRead)
            {
                while (true)
                {
                    if (stream.DataAvailable)
                    {
                        return await stream.ReadAsync(buffer, offset, count, cts.Token).ConfigureAwait(false);
                    }

                    if (keepTrying)
                    {
                        await Task.Delay(300, cts.Token).ConfigureAwait(false);
                    }
                    else
                    {
                        cts.Dispose();
                        timer.Dispose();
                        throw new IOException();
                    }
                }
            }
        }
        catch (TaskCanceledException tce)
        {
            // do nothing
        }
        finally
        {
            cts.Dispose();
            timer.Dispose();
        }
        if (stream.DataAvailable)
        {
            return await stream.ReadAsync(buffer, offset, count).ConfigureAwait(false);
        }

        throw new IOException();
    }

    private static void stopTrying(object sender, ElapsedEventArgs args, CancellationTokenSource cts, out bool keepTrying)
    {
        keepTrying = false;
        cts.Cancel();
    }

}

应用程序必须能够与数千个端点进行通信,并且我希望以不会阻塞大量线程的方式创建它,因为它所做的大部分工作都是IO。此外,超时的情况应该是

3 个答案:

答案 0 :(得分:3)

首先,您尝试做的事情从根本上是有缺陷的。你应该一直从一个开放的TCP / IP流读取 - 只要一次读取获取一些数据,然后传递它并开始下一次读取。

所以,我的第一个建议是首先需要一个可取消的读取。相反,始终保持阅读。同样,使用DataAvailable是代码气味。

更多解释......

没有一种好的方法可以强制执行&#34;取消不可取消的代码。关闭TCP / IP套接字是最简单,最干净的方法。您现有的解决方案无法正常工作,因为ReadAsync忽略了CancellationToken。所以它没有比仅使用CancellationToken没有计时器更好的了。如果ReadAsync忽略CancellationToken,则唯一真正的选择是关闭套接字。任何其他解决方案都可能导致数据丢失&#34; - 从套接字读取但丢弃的数据。

答案 1 :(得分:0)

对于类似的用例,我使用Task.Delay()任务进行超时。 看起来像这样:

public static async Task<int> ReadAsync(
        NetworkStream stream, byte[] buffer, int offset, int count, int timeoutMillis)
{
        if (timeoutMillis < 0) throw new ArgumentException(nameof(timeoutMillis));
        else if (timeoutMillis == 0)
        {
            // No timeout
            return await stream.ReadAsync(buffer, offset, count);
        }

        var cts = new CancellationTokenSource();
        var readTask = stream.ReadAsync(buffer, offset, count, cts.Token);
        var timerTask = Task.Delay(timeoutMillis, cts.Token);

        var finishedTask = await Task.WhenAny(readTask, timerTask);
        var hasTimeout = ReferenceEquals(timerTask, finishedTask);
        // Cancel the timer which might be still running
        cts.Cancel();
        cts.Dispose();

        if (hasTimeout) throw new TimeoutException();
        // No timeout occured
        return readTask.Result;
}

答案 2 :(得分:0)

基于discussionStephen Cleary以及他的建议,我再次看看我如何实现这一点,并采用了一种不会因读取而超时的方法,但它保持开放状态只要TcpClient打开然后从不同的代码控制超时。我使用Task.Run(() => beginReading());所以当然会使用池中的线程,但我认为这是可以的,因为大多数时候该线程将命中await因此是免费的

这是我的实施:

private readonly Queue<byte> bigBuffer = new Queue<byte>();
private readonly SemaphoreSlim _signal = new SemaphoreSlim(0, 1);

// This is called in a Task.Run() 
private async Task beginReading()
{
    byte[] buffer = new byte[1024];

    using (_shutdownToken.Register(() => m_TcpStream.Close()))
    {
        while (!_shutdownToken.IsCancellationRequested)
        {
            try
            {
                int bytesReceived = 0;
                if (m_TcpStream.CanRead)
                {
                    bytesReceived = await m_TcpStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
                }
                else
                {
                    // in case the stream is not working, wait a little bit
                    await Task.Delay(3000, _shutdownToken);
                }

                if (bytesReceived > 0)
                {
                    for (int i = 0; i < bytesReceived; i++)
                    {
                        bigBuffer.Enqueue(buffer[i]);
                    }

                    _signal.Release();
                    Array.Clear(buffer, 0, buffer.Length);

                }
            }
            catch (Exception e)
            {
                LoggingService.Log(e);
            }
        }
    }
}

private async Task<int> ReadAsyncWithTimeout(byte[] buffer, int offset, int count)
{
    int bytesToBeRead = 0;

    if (!m_TcpClient.Connected)
    {
        throw new ObjectDisposedException("Socket is not connected");
    }

    if (bigBuffer.Count > 0)
    {
        bytesToBeRead = bigBuffer.Count < count ? bigBuffer.Count : count;

        for (int i = offset; i < bytesToBeRead; i++)
        {
            buffer[i] = bigBuffer.Dequeue();
        }

        // Clear up the semaphore in case of a race condition where the writer just wrote and then this came in and read it without waiting
        if (_signal.CurrentCount > 0)
            await _signal.WaitAsync(BabelfishConst.TCPIP_READ_TIME_OUT_IN_MS, _shutdownToken).ConfigureAwait(false);

        return bytesToBeRead;
    }

    // In case there is nothing in the Q, wait up to timeout to get data from the writer
    await _signal.WaitAsync(15000, _shutdownToken).ConfigureAwait(false);

    // read again in case the semaphore was signaled by an Enqueue
    if (bigBuffer.Count > 0)
    {
        bytesToBeRead = bigBuffer.Count < count ? bigBuffer.Count : count;

        for (int i = offset; i < bytesToBeRead; i++)
        {
            buffer[i] = bigBuffer.Dequeue();
        }


        return bytesToBeRead;
    }

    // This is because the synchronous NetworkStream Read() method throws this exception when it times out
    throw new IOException();
}