ToB企服应用市场:ToB评测及商务社交产业平台

标题: dotnet 已知问题 鉴戒 StreamReader 的 EndOfStream 卡住线程 [打印本页]

作者: 杀鸡焉用牛刀    时间: 2024-9-5 08:07
标题: dotnet 已知问题 鉴戒 StreamReader 的 EndOfStream 卡住线程
在 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 文件夹,即可获取到源代码
更多技术博客,请参阅 博客导航

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4