【C# 序列化】 ReadOnlySequence


轉自:https://www.cnblogs.com/TianFang/p/10084049.html

序列Sequences)

數學中是指被排成一列的對象事件

 

例如,(C,Y,R)是一個字母的序列:順序是C第一,Y第二,R第三。序列可以是有限的(就像前面這個例子),也可以是無限的,就像所有正偶數的序列(2,4,6,...)。有限序列包含空序列(),它沒有元素。序列中的元素也稱為,項的個數(可能是無限的)稱為序列的長度

序列寫作(a1,a2, ...)。簡單起見,也可以用符號(an)。

有限的序列稱為列表(lists)。有限的字符串序列稱為字符串(string)。無限的序列稱為字符串流(stream)。

 

ReadOnlySequenceSegment<T>

 

 

在我們讀取數據的過程,很多時候會出現如下場景:

  1. 不知道數據實際大小
  2. 一次性申請大量內存開銷太大

此時我們往往會使用動態內存的方案,通過鏈表的方式串聯起來,從而形成邏輯意義上的數據流。如下圖所示:

ReadOnlySequenceSegment<T>就是這樣一個表示數據流節點的內存模型,它是一個抽象類,包含如下三個元素:

  •  
    1 public ReadOnlyMemory<T> Memory { get; protected set; } 2 public ReadOnlySequenceSegment<T>? Next { get; protected set; } 3 public long RunningIndex { get; protected set; }
  • Memory:表示這個鏈表節點下的內存數據,也就是上面的Memory1、2、3
  • Next:就是指向的下一個節點
  • RunningIndex:指當前節點之前的節點的數據之和,比如Memory1里有1個字節、Memeory2里有2個字節,那么Memory3對應節點的RunningIndex就是3

這玩意是個抽象類,不過暫時可以不關心,因為我們通常開發時都可以從某個方法的參數獲得ReadOnlySequenceSegment<T>(下面馬上會說),而它里面就保存着這個鏈表的收尾兩個節點。

這里重點記住:

  • ReadOnlySequenceSegment里面存儲的ReadOnlyMemory<T>(理解上約等於byte[])
  • 多個ReadOnlySequenceSegment可以組成一個鏈表,從邏輯上表示一個完整的數據,ReadOnlySequenceSegment只是其中一個節點

其中Memory和Next還比較容易理解,典型的鏈表結構。主要難理解的是RunningIndex,他表示該節點在數據流中的Memory起始索引。

一般的來講,某節點的RunningIndex為其上一個節點的RunningIndex + Memory.Length。加上RunningIndex估計主要是為了快速索引的。

例如:對於如下3快內存 100byte, 200byte, 300byte組成的鏈表,其RunningIndex分別是0, 100, 200。

另外,在實際的使用過程中,往往是不停的釋放鏈表頭部的節點,並且在尾部添加新節點。 RunningIndex表示的索引一般是邏輯意義上的索引,在釋放頭節點時,一般不用更新其子節點以及后續節點的RunningIndex。

 

ReadOnlySequence<T>

 

 

ReadOnlySequenceSegment<T>雖然能解決我們的動態內存的申請和釋放問題,但它往往並不好用,因為很容易出現一段連續的數據被分割在多個節點的情況,在這段不連續的數據里進行查詢是非常不便的。

為了解決這個問題,.net core中推出了一個視圖類ReadOnlySequence<T>

ReadOnlySequence<T>由兩個屬性標記:

  • Start: 起始SequenceSegment以及起始索引
  • End: 結尾SequenceSegment以及結尾索引

可以通過foreach遍歷各節點的Memory

  var seq = new ReadOnlySequence<byte>();   foreach (ReadOnlyMemory<byte> memory in seq)   {   } 

ReadOnlySequence的主要優勢在於,它可以看成一段邏輯意義上的連續內存,常用的函數有:

  • Slice: 對視圖數據切片
  • PositionOf: 查詢元素的縮影
  • ToArray: 轉換成數組

其中的ToArray涉及到大量的數據拷貝,需要謹慎使用。

另外.net core 3.0中還內置了一個SequenceReader,用起來是十分方便的: 

復制代碼
private static ReadOnlySpan<byte> CRLF => new byte[] { (byte)'\r', (byte)'\n' }; public static void ReadLines(ReadOnlySequence<byte> sequence) { SequenceReader<byte> reader = new SequenceReader<byte>(sequence); while (!reader.End) { if (!reader.TryReadToAny(out ReadOnlySpan<byte> line, CRLF, advancePastDelimiter: false)) { // Couldn't find another delimiter // ...  } if (!reader.IsNext(CRLF, advancePast: true)) { // Not a good CR/LF pair // ...  } // line is valid, process  ProcessLine(line); } }
復制代碼

 

關於.net core高性能編程中的Span<T>和Memory<T>網上資料很多,這里就不說了。今天一直在看ReadOnlySequenceSegment<T>和SequenceReader<T>,看得腦殼痛,本篇着重說說對ReadOnlySequenceSegment<T>的理解。

如果對Span<T>和Memory<T>不了解,可以暫時理解為byte[],最好先去搜下相關資料。緩沖區相關知識可以參考官方文檔:https://docs.microsoft.com/zh-cn/dotnet/standard/io/buffers

內存片段ReadOnlySequenceSegment<T>

假設你已經了解了Memory<T>,它表示一段連續的內存,有時候我們讀取一條數據,它可能並不是存在連續的內存中。

 

這個我理解得不是很准確,但總體來說就是我們一個完整的數據分成了多個內存片段,每個內存片段用Memory<byte>(你也可以暫時理解為byte[])表示,那么可以以鏈表的形式,從邏輯上來表示這段完整的數據。比如Memory1上有個next屬性指向Memory2,同理Memory2上的next屬性指向Memory3,這樣的鏈表就能表示這段完整的數據了。

ReadOnlySequenceSegment<T>就是這樣一個鏈表,3個核心屬性定義如下:

1 public ReadOnlyMemory<T> Memory { get; protected set; } 2 public ReadOnlySequenceSegment<T>? Next { get; protected set; } 3 public long RunningIndex { get; protected set; }
  • Memory:表示這個鏈表節點下的內存數據,也就是上面的Memory1、2、3
  • Next:就是指向的下一個節點
  • RunningIndex:指當前節點之前的節點的數據之和,比如Memory1里有1個字節、Memeory2里有2個字節,那么Memory3對應節點的RunningIndex就是3

這玩意是個抽象類,不過暫時可以不關心,因為我們通常開發時都可以從某個方法的參數獲得ReadOnlySequenceSegment<T>(下面馬上會說),而它里面就保存着這個鏈表的收尾兩個節點。

這里重點記住:

  • ReadOnlySequenceSegment里面存儲的ReadOnlyMemory<T>(理解上約等於byte[])
  • 多個ReadOnlySequenceSegment可以組成一個鏈表,從邏輯上表示一個完整的數據,ReadOnlySequenceSegment只是其中一個節點

內存片段容器ReadOnlySequence<T>

上面說的這個內存片段鏈表其實已經可以從邏輯上表示一段完整的數據了,但是ReadOnlySequenceSegment<T>只是這個鏈表中的一個節點,它能提供的屬性、方法等api只能針對自己這個節點,所以需要一個容器來容納整個鏈表,以提供對此連續內存片段操作的api

這里說的容器不是很准確,因為ReadOnlySequence只是存儲了整個鏈表的首位節點,但是由於是鏈表,其實只要知道首節點,就可以通過Next遞歸獲得整個鏈表的所有節點,因此我這里把它稱為容器

下面引用官方文檔的一張圖

綠色框中有3段藍色塊,我們可以理解為是鏈表中的一個節點(ReadOnlySequenceSegment),由於這個節點內部重要的就是保存着具體的數據Memory<T>,所以我們可以簡單的看成是3個Memory<T>,這里便於理解,也可以看成是3個byte[]。
根據綠色部分的3個不連續的內存片段,可以生成一個表示邏輯上連續的內存片段集合ReadOnlySequence,這個ReadOnlySequence包含3個Memory<T>,其中首位的片段只取原始片段的一部分。下面我根據理解再來一張圖

 

注:上面簡寫的16進制,A=0x0A

連續內存片段中的索引SequencePosition

只要知道一個數據在哪個片段中,並且知道它在這個片段中的哪個位置,就能表示一個具體的索引了。

但特別注意這個索引是針對原始鏈表來說的,也就是上面綠色快的部分,比如圖片中的“4”在第1段的索引3的位置;“A”,在第2段的索引2處。這種情況沒有辦法用單個數字來表示索引,因此單獨定義了SequencePosition來表示索引。

ReadOnlySequence的api

  • 構造函數ReadOnlySequence(ReadOnlySequenceSegment<T> startSegment, int startIndex, ReadOnlySequenceSegment<T> endSegment, int endIndex)
    • startSegment:鏈表的首個節點
    • startIndex:首個節點不一定完全加入到ReadOnlySequence,此參數表示從第幾個值開始
    • endSegment:鏈表的尾節點
    • endIndex:尾節點也不一定完全加入ReadOnlySequence,此參數表示要加入的索引+1
    • 按上圖所示,代碼應該這樣:new  ReadOnlySequence(片段1,3,片段3,1); 注意最后一個參數是1,可以簡單理解為在尾節點取前幾個值加入到ReadOnlySequence
  • End:就是最后一個片段的最后一個數據的索引對象,就是圖片中的片段3索引1
  • Start:第一個片段的索引,片段1,索引2
  • Length:ReadOnlySequence包含的值的長度,按圖中就是4 5 6 ....D F 2  長度為10
  • GetPosition(int index):獲取第幾個值的索引對象,比如GetPosition(0),那就是黃色塊的0為4,它所處於綠色塊的索引為:片段1,索引2;GetPosition(4),那就是黃色塊的2,所處綠色快的片段2,索引1
  • PositionOf(T value):查早某個值在這個序列中所處的索引,比如PositionOf(4),那就是在黃色塊的片段1的索引0處,最終結果就是綠色塊片段1的索引3處
  • Slice():從這個連續內存片段集合中指定索引處開始,取一段數據,返回的是一個新的ReadOnlySequence。有幾個重載,比較容易猜到它的意義
  •  bool TryGet(ref SequencePosition position, out ReadOnlyMemory<T> memory, bool advance = true) 

    嘗試從指定索引處開始讀取,所指定的索引處所在片段還有剩余數據,則本次讀取這些剩余數據,否則讀取下一個片段的數據。最終若讀取成功,則返回true,且將讀取到的數據賦值給memory參數。advance為true時,position將被直接賦值為下一個片段的索引0處。理解這個再看官方文檔那個循環就容易了。

主要api就這幾個。

SequenceReader<T>

.net core 3.x提供了SequenceReader來幫我們更容易的讀取ReadOnlySequence的數據。我們只要理解一點就能很容易的理解此對象。先看看下圖

 

 

這里我取名叫“已讀索引”,就是表示已經讀取過了。SequenceReader.Advance(2)就是將這個索引往后移2位,SequenceReader.Rewind(1)則表示將這個索引前移1位。

以圖中為例,若此時調用TryRead方法,則將獲取第3個位的數據,並且已讀索引向后移1位

 

 

理解這個再看官方文檔就簡單了,舉幾個例子

  • Consumed:表示已經讀取了多數個數據,也就是“已讀索引”之前有幾個數據
  • Remaining:表示整個序列還剩幾個數據,也就是“已讀索引”之后有幾個數據
  • Advance(Int64):將“已讀索引”移動到指定位置
  • AdvancePast(T):將"已讀索引"移動到指定值所在的位置
  • TryRead(T):嘗試從“已讀索引”開始讀取1個值,並將“已讀索引”向后移動1位
  • TryPeek(T):嘗試從“已讀索引”開始讀取1個值,但不移動“已讀索引”
  • TryReadBigEndian():嘗試從“已讀索引”開始讀取4個值,並將其轉換位int類型的值,然后將“已讀索引”向后移動4位

如何使用

用過System.IO.Pipelines的朋友就知道,ReadOnlySequence在該庫中是非常好用的。但如果我們想創建一個ReadOnlySequence,發現並不是那么容易,因為:

  1. ReadOnlySequence依賴於ReadOnlySequenceSegment
  2. ReadOnlySequenceSegment是抽象類,需要自己繼承

也就是說我們需要自己實現ReadOnlySequenceSegment<T>,然后再將其封裝到ReadOnlySequence中,目前.net core中並沒有內置實現可能是因為在高效內存管理的方案中並沒有什么通用的解決方案吧。

如果我們要自己實現ReadOnlySequence,一般需要如下幾個步驟:

  1. 繼承ReadOnlySequenceSegment類,實現自己的SequenceSegment
  2. 在申請內存過程中,創建SequenceSegment,並將其掛成鏈表
  3. 使用數據時,在該鏈表中創建ReadOnlySequence
  4. 當SequenceSegment節點的內存使用完成的時候,從鏈表中接觸該節點,並釋放內存。

簡單來說就是如下幾種操作:

  • 數據讀取: 創建SequenceSegment
  • 數據使用: 在SequenceSegment鏈表上創建ReadOnlySequence
  • 使用完成: 釋放SequenceSegment

如果要更進一步優化,在SequenceSegment中的內存申請和釋放可以使用內存池。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM