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

news/2024/9/22 20:22:17/文章来源:https://www.cnblogs.com/lindexi/p/18397603

在 dotnet 里面,咱会经常使用 StreamReader 辅助类读取 Stream 的内容,比如按行读取等。如果在判断是否读取完成时,使用的是 StreamReader 的 EndOfStream 属性,则可能破坏原本的异步出让逻辑,导致线程被卡住

对于带 UI 的应用程序,如 WPF 等应用来说,如果 UI 线程被卡住,可能会是一个比较重的坑。在 dotnet 里面的 StreamReader 类里面的 EndOfStream 存在一个设计上的问题。访问 EndOfStream 会导致 StreamReader 执行一次同步读取 Stream 的过程

假定 Stream 是一个读取非常慢的对象,如卡顿的网络下的响应内容。此时使用 StreamReader 类进行异步读取,自然不会卡住线程。假定异步读取的是 ReadLineAsync 按行读取,那开发者可能的需求是知道读取完成,常见错误的写法如下

var streamReader = new StreamReader(...);// 这是错误的实现,错误使用 EndOfStream 作为循环判断条件
while (!streamReader.EndOfStream)
{var line = await streamReader.ReadLineAsync();// 忽略其他代码
}

以上代码是错误的实现方式,核心原因是在判断是否已经读取完成使用了 EndOfStream 属性而不是 ReadLineAsync 的返回值

正确的实现应该是如下

while (true)
{var line = await streamReader.ReadLineAsync();if (line is null){break;}
}

在 ReadLineAsync 或 ReadLine 方法里面,如果一行里面是空文本,则会返回 "" 空字符串。当读取完成的时候,则会返回 null

当然了,使用 ReadLine 方法读取的时候,使用 EndOfStream 属性是没有什么问题的,因为本身就在进行同步读写

为什么在使用 ReadLineAsync 异步方法时,不能使用 EndOfStream 属性作为循环结束条件?通过读 dotnet 的实现源代码可以看到 EndOfStream 属性是通过读取一下,看看是不是读取完了,如果读取完就返回 true 的值,否则就继续返回 false 的值

由于 C# 的属性从语法上就不支持异步方法,导致 EndOfStream 属性只能进行同步读取,从而导致 EndOfStream 属性可能卡线程。从 C# 属性设计上讲,通用的属性应该都是获取速度十分快的,然而 EndOfStream 属性违背了这一点,居然是进行同步读取 Stream 内容才能判断,这就导致了如果 StreamReader 所读取的 Stream 是缓慢的,将会导致 EndOfStream 属性返回缓慢

接下来我将编写一个简单的测试代码用于告诉大家使用 EndOfStream 属性在进行异步读取时的缺点

如下面代码,编写了一个 FooStream 类型,这个类型在读取的时候速度非常缓慢

class FooStream : Stream
{public FooStream(){_buffer = "123\r\n"u8.ToArray();}private readonly byte[] _buffer;public override void Flush(){}public override int Read(byte[] buffer, int offset, int count){// 模拟卡顿Thread.Sleep(10000);if (count >= _buffer.Length){count = _buffer.Length;Array.Copy(_buffer, 0, buffer, offset, count);}return count;}public override long Seek(long offset, SeekOrigin origin){return offset;}public override void SetLength(long value){}public override void Write(byte[] buffer, int offset, int count){}public override bool CanRead => true;public override bool CanSeek => false;public override bool CanWrite => false;public override long Length => long.MaxValue;public override long Position { get; set; }
}

如以下代码,使用 StreamReader 进行异步读取,且错误使用 EndOfStream 属性作为判断条件

var fooStream = new FooStream();
var streamReader = new StreamReader(fooStream);while (!streamReader.EndOfStream)
{var line = await streamReader.ReadLineAsync();if (line is null){break;}
}

尝试跑起来代码,可以看到在 EndOfStream 属性获取时卡住,在 Visual Studio 里点击暂停,在堆栈窗口可以看到如下代码

> 	System.Private.CoreLib.dll!System.Threading.Thread.Sleep(int millisecondsTimeout)HerrigeedaJardarkewel.dll!FooStream.Read(byte[] buffer, int offset, int count)System.Private.CoreLib.dll!System.IO.StreamReader.ReadBuffer()System.Private.CoreLib.dll!System.IO.StreamReader.EndOfStream.get()HerrigeedaJardarkewel.dll!Program.<Main>$(string[] args)HerrigeedaJardarkewel.dll!Program.<Main>(string[] args)

阅读 dotnet 的源代码,可以看到 EndOfStream 属性的实现如下

namespace System.IO
{// This class implements a TextReader for reading characters to a Stream.// This is designed for character input in a particular Encoding,// whereas the Stream class is designed for byte input and output.public class StreamReader : TextReader{public bool EndOfStream{get{ThrowIfDisposed();CheckAsyncTaskInProgress();if (_charPos < _charLen){return false;}// This may block on pipes!int numRead = ReadBuffer();return numRead == 0;}}internal virtual int ReadBuffer(){... // 忽略其他代码int len = _stream.Read(_byteBuffer, _bytePos, _byteBuffer.Length - _bytePos);... // 忽略其他代码}... // 忽略其他代码}
}

从上面代码可以看到 EndOfStream 是通过判断 ReadBuffer 是否能够读取到内容从而判断是否已经读取完成

在 ReadBuffer 方法里面将执行 _stream.Read 同步的读取方法。如果此时 _stream 的读取缓慢,则会卡住线程

本文代码放在 github 和 gitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 96a09bc149186f9122f263f887257dcbf209d4e3

以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 96a09bc149186f9122f263f887257dcbf209d4e3

获取代码之后,进入 Workbench/HerrigeedaJardarkewel 文件夹,即可获取到源代码

更多技术博客,请参阅 博客导航

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/792385.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用 Dependify 工具探索 .NET 应用程序依赖项

在大型项目中,由于各种组件的复杂性和互连性,管理依赖项可能变得具有挑战性。如果没有适当的工具或文档,可能很难浏览项目并对依赖项做出假设。以下是在大型项目中难以导航项目依赖项的几个原因:复杂性:大型项目通常由许多模块组成。了解这些依赖项如何相互交互可能会让人…

读软件设计的要素01概念

概念1. 概念 1.1. 一个软件,从运行于手机上的最小程序到大型的企业系统,都是由概念组成的,每个概念都是独立的功能单元 1.2. 软件中的可用性问题,经常可以追溯到其底层概念1.2.1. 概念帮助识别软件的不可用性1.3. 概念都是以同样形式在各种软件中重复使用的1.3.1. 将设计分…

关于在powershell与cmd查找vue版本时结果不一致

在powershell中vue版本显示为2.9.6而在Cmd中显示为@vue/cli 5.0.8原因:这是由于博主先前旧版本下载了vue2.9.6,而在卸载旧版本2.9.6时候仅删除了vue与vue.cmd文件 而powershell会先运行vue.ps1(.ps1后缀为powershell文件)来获取你的版本号 解决方案:将vue,vue.cmd,vue.ps1…

LUNARiA

本文同步发布于我的网站也算是头一次在没有任何安利和剧透,仅在看了简介的情况下就直接下单并开始游玩一部gal了。果然,没有给我留下什么遗憾呢。 游玩日志 SKYOUT-FOREVER 《LUNARiA》的背景设定在科技腾飞、算力大幅增长、AI与VR遍及各行各业、一个名为Cozmo的虚拟世界已经…

Comfyui 基础教程(一) —— 本地安装部署

前言 前面一篇文章已经介绍过,ComfyUI 和 Stable Diffusion 的关系。不清楚的朋友,看传送门 Stable Diffusion 小白的入坑铺垫 。 WebUI 以及 ComfyUI 不等于 Stable Diffusion,可以简单粗暴一点的理解为方便运行某些大模型的工具。由于本人在接触过 ComfyUI 之后,就基本放…

337. 打家劫舍 III(leetcode)

https://leetcode.cn/problems/house-robber-iii/description/基础树形dp,要点是f的定义灵神讲的很好:https://www.bilibili.com/video/BV1vu4y1f7dn/?vd_source=1bb76d0400eba0d49ad4c1dd07bc4249 /*** Definition for a binary tree node.* public class TreeNode {* in…

深度学习环境配置(windows 11)

安装Anaconda下载地址运行安装程序,记得要勾选创建系统变量,忘记勾选的请参考anaconda如何配置环境变量。正确配置后,在cmd命令行中输入conda --version后可以输出conda版本信息。附上conda常用操作命令: #创建环境(指定python版本) conda create -n NAME python=*.*#激活…

Text Augmented Spatial-aware Zero-shot Referring Image Segmentation论文阅读笔记(EMNLP23 Findings)

Motivation & Method 关注的任务为zero-shot referring image segmentation,模型无法获得pixel-level的分割标注。之前的方法通常使用预训练的多模态模型如CLIP,然而CLIP使用图像文本对进行训练,难以做到image local patch与referring sentence的细粒度对齐。为此作者提…

Error response from daemon: This node is not a swarm manager.

转载请注明出处:在环境上通过 docker 查看节点列表时,报错如下:解决方法: 具体code如下:root@controller1:~# docker swarm init --advertise-addr 127.0.0.1 Error response from daemon: This node is already part of a swarm. Use "docker swarm leave" to …

全网最适合入门的面向对象编程教程:45 Python 实现常见数据结构-链表、树、哈希表、图和堆

数据结构是计算机科学中的一种组织和存储数据的方式,它决定了数据的访问方式和操作效率,数据结构的选择和实现对程序的性能和设计至关重要。本文主要讲述了如何使用Python语言和内置库实现常见数据结构。全网最适合入门的面向对象编程教程:45 Python 实现常见数据结构-链表、…

Unicode编码介绍

什么是Unicode编码? Unicode是字符编码规范,它定义了所有文字的编码规则。说通俗点就是它按照某个规则给每个字符都分配了一个数字编号(比如:A的编号为65或0x0041,万的编号为19981或0x4e0d),相当于是一个编号库。这个数字编号也叫Unicode码。最开始这个编号使用2个字节表…

LeetCode题集-2 - 两数相加

递归法和迭代法有什么差别,通过两数相加算法来搞明白,一举多得这个题目是什么意思呢?简单来说就是把两个链表平铺开,头节点对齐,然后从头开始相同的节点相加,满10则进位,进位值与下个节点继续相加,当一个链表没有节点时候则可以把没有节点当做0继续与有节点的链表继续相…