翼度科技»论坛 编程开发 .net 查看内容

dotnet 已知问题 警惕 StreamReader 的 EndOfStream 卡住线程

5

主题

5

帖子

15

积分

新手上路

Rank: 1

积分
15
在 dotnet 里面,咱会经常使用 StreamReader 辅助类读取 Stream 的内容,比如按行读取等。如果在判断是否读取完成时,使用的是 StreamReader 的 EndOfStream 属性,则可能破坏原本的异步出让逻辑,导致线程被卡住
对于带 UI 的应用程序,如 WPF 等应用来说,如果 UI 线程被卡住,可能会是一个比较重的坑。在 dotnet 里面的 StreamReader 类里面的 EndOfStream 存在一个设计上的问题。访问 EndOfStream 会导致 StreamReader 执行一次同步读取 Stream 的过程
假定 Stream 是一个读取非常慢的对象,如卡顿的网络下的响应内容。此时使用 StreamReader 类进行异步读取,自然不会卡住线程。假定异步读取的是 ReadLineAsync 按行读取,那开发者可能的需求是知道读取完成,常见错误的写法如下
  1. var streamReader = new StreamReader(...);
  2. // 这是错误的实现,错误使用 EndOfStream 作为循环判断条件
  3. while (!streamReader.EndOfStream)
  4. {
  5.     var line = await streamReader.ReadLineAsync();
  6.     // 忽略其他代码
  7. }
复制代码
以上代码是错误的实现方式,核心原因是在判断是否已经读取完成使用了 EndOfStream 属性而不是 ReadLineAsync 的返回值
正确的实现应该是如下
  1. while (true)
  2. {
  3.     var line = await streamReader.ReadLineAsync();
  4.     if (line is null)
  5.     {
  6.         break;
  7.     }
  8. }
复制代码
在 ReadLineAsync 或 ReadLine 方法里面,如果一行里面是空文本,则会返回 "" 空字符串。当读取完成的时候,则会返回 null 值
当然了,使用 ReadLine 方法读取的时候,使用 EndOfStream 属性是没有什么问题的,因为本身就在进行同步读写
为什么在使用 ReadLineAsync 异步方法时,不能使用 EndOfStream 属性作为循环结束条件?通过读 dotnet 的实现源代码可以看到 EndOfStream 属性是通过读取一下,看看是不是读取完了,如果读取完就返回 true 的值,否则就继续返回 false 的值
由于 C# 的属性从语法上就不支持异步方法,导致 EndOfStream 属性只能进行同步读取,从而导致 EndOfStream 属性可能卡线程。从 C# 属性设计上讲,通用的属性应该都是获取速度十分快的,然而 EndOfStream 属性违背了这一点,居然是进行同步读取 Stream 内容才能判断,这就导致了如果 StreamReader 所读取的 Stream 是缓慢的,将会导致 EndOfStream 属性返回缓慢
接下来我将编写一个简单的测试代码用于告诉大家使用 EndOfStream 属性在进行异步读取时的缺点
如下面代码,编写了一个 FooStream 类型,这个类型在读取的时候速度非常缓慢
  1. class FooStream : Stream
  2. {
  3.     public FooStream()
  4.     {
  5.         _buffer = "123\r\n"u8.ToArray();
  6.     }
  7.     private readonly byte[] _buffer;
  8.     public override void Flush()
  9.     {
  10.     }
  11.     public override int Read(byte[] buffer, int offset, int count)
  12.     {
  13.         // 模拟卡顿
  14.         Thread.Sleep(10000);
  15.         if (count >= _buffer.Length)
  16.         {
  17.             count = _buffer.Length;
  18.             Array.Copy(_buffer, 0, buffer, offset, count);
  19.         }
  20.         return count;
  21.     }
  22.     public override long Seek(long offset, SeekOrigin origin)
  23.     {
  24.         return offset;
  25.     }
  26.     public override void SetLength(long value)
  27.     {
  28.     }
  29.     public override void Write(byte[] buffer, int offset, int count)
  30.     {
  31.     }
  32.     public override bool CanRead => true;
  33.     public override bool CanSeek => false;
  34.     public override bool CanWrite => false;
  35.     public override long Length => long.MaxValue;
  36.     public override long Position { get; set; }
  37. }
复制代码
如以下代码,使用 StreamReader 进行异步读取,且错误使用 EndOfStream 属性作为判断条件
  1. var fooStream = new FooStream();
  2. var streamReader = new StreamReader(fooStream);
  3. while (!streamReader.EndOfStream)
  4. {
  5.     var line = await streamReader.ReadLineAsync();
  6.     if (line is null)
  7.     {
  8.         break;
  9.     }
  10. }
复制代码
尝试跑起来代码,可以看到在 EndOfStream 属性获取时卡住,在 Visual Studio 里点击暂停,在堆栈窗口可以看到如下代码
  1. >         System.Private.CoreLib.dll!System.Threading.Thread.Sleep(int millisecondsTimeout)
  2.         HerrigeedaJardarkewel.dll!FooStream.Read(byte[] buffer, int offset, int count)
  3.         System.Private.CoreLib.dll!System.IO.StreamReader.ReadBuffer()
  4.         System.Private.CoreLib.dll!System.IO.StreamReader.EndOfStream.get()
  5.         HerrigeedaJardarkewel.dll!Program.<Main>$(string[] args)
  6.         HerrigeedaJardarkewel.dll!Program.<Main>(string[] args)
复制代码
阅读 dotnet 的源代码,可以看到 EndOfStream 属性的实现如下
  1. namespace System.IO
  2. {
  3.     // This class implements a TextReader for reading characters to a Stream.
  4.     // This is designed for character input in a particular Encoding,
  5.     // whereas the Stream class is designed for byte input and output.
  6.     public class StreamReader : TextReader
  7.     {
  8.         public bool EndOfStream
  9.         {
  10.             get
  11.             {
  12.                 ThrowIfDisposed();
  13.                 CheckAsyncTaskInProgress();
  14.                 if (_charPos < _charLen)
  15.                 {
  16.                     return false;
  17.                 }
  18.                 // This may block on pipes!
  19.                 int numRead = ReadBuffer();
  20.                 return numRead == 0;
  21.             }
  22.         }
  23.         internal virtual int ReadBuffer()
  24.         {
  25.                  ... // 忽略其他代码
  26.             int len = _stream.Read(_byteBuffer, _bytePos, _byteBuffer.Length - _bytePos);
  27.              ... // 忽略其他代码
  28.         }
  29.         ... // 忽略其他代码
  30.     }
  31. }
复制代码
从上面代码可以看到 EndOfStream 是通过判断 ReadBuffer 是否能够读取到内容从而判断是否已经读取完成
在 ReadBuffer 方法里面将执行 _stream.Read 同步的读取方法。如果此时 _stream 的读取缓慢,则会卡住线程
本文代码放在 githubgitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快
先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
  1. git init
  2. git remote add origin https://gitee.com/lindexi/lindexi_gd.git
  3. git pull origin 96a09bc149186f9122f263f887257dcbf209d4e3
复制代码
以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码
  1. git remote remove origin
  2. git remote add origin https://github.com/lindexi/lindexi_gd.git
  3. git pull origin 96a09bc149186f9122f263f887257dcbf209d4e3
复制代码
获取代码之后,进入 Workbench/HerrigeedaJardarkewel 文件夹,即可获取到源代码
更多技术博客,请参阅 博客导航

来源:https://www.cnblogs.com/lindexi/p/18397603
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具