在 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 資料夾,即可獲取到原始碼
更多技術部落格,請參閱 部落格導航