本來想寫篇關於System.Collections.Immutable中提供的ImmutableList里一些實現細節來着,結果一時想不起來源碼在哪里——為什么會變成這樣呢……第一次有了想寫分析的源碼,又有了寫博客的時間。兩件快樂事情重合在一起。而這兩份快樂,又給我帶來更多的快樂。得到的,本該是像夢境一般幸福的時間……但是,為什么,會變成這樣呢……還好順路看到MS開源的一個基於內存池的MemoryStream替代實現,看起來用這個水一篇文章妥妥的。
ps: 雖然在標題上扯了.net,不過說實話除了代碼是用c#外,我自己也想不出來其中的技術概念與.net有幾毛錢關系。如果願意的話,
絕大部分語言都可以實現來着。
var ms = new MemoryStream()
在討論對象池之前,咱們先來看一段簡單的代碼。
var ms = new MemoryStream();
DataContractSerializer serializer = new DataContractSerializer(typeof(Model));
serializer.WriteObject(ms, model);
這段代碼采用DataContractSerializer序列化一個類型為Model的對象,沒有什么特殊的技巧可言。大部分情況下也沒必要去折騰這塊代碼。
不過,在實際系統中,此處還是存在着一個不大不小的問題——每次運行這個代碼段都會新建一個MemoryStream,然后往這個MemoryStream中寫入字節流。
看上去MemoryStream非常神奇,它是一個基於內存,可以不斷寫入(只要還有內存)的流。不過如果查看源碼,就會發現它實際上還是基於byte[]
實現的,每次擴展容量時,都新建一個足夠大的byte[]
:
if (_expandable && value != _capacity) {
if (value > 0) {
byte[] newBuffer = new byte[value];
if (_length > 0) Buffer.InternalBlockCopy(_buffer, 0, newBuffer, 0, _length);
_buffer = newBuffer;
}
else {
_buffer = null;
}
_capacity = value;
}
別的不說,默默新建的那一堆byte[]
對GC產生的壓力,甚至有可能觸發過多的gen2 gc,對提高系統的吞吐量都是非常不利的。最好能重復利用已經分配出來的內存空間,讓這些byte[]
活的越久越好。於是我們就得把用完的MemoryStream回收回來,洗洗更健康再用。
對象池
其實對象池的基本思路非常簡單,無非分為以下幾步:
- 創建池
- 使用者向池申請資源
- 池分配可用的對象(如果沒有現成的,則創建個新對象)
- 使用者用完后將對象還回池中
無論是ADO.NET中的連接池,還是ThreadPool線程池,抑或接下來要說到的RecyclableMemoryStream內存池,都離不開
這個思路。
下載源碼
由於原版的RecyclableMemoryStream中的功能完整,不太適合講解對象池的原理之用,故特地裁剪了個
版本出來——雖然也能用,不過在實際項目中還是推薦用nuget安裝完整版的。
簡化版請點此處
static RecyclableMemoryStreamManager recyclableMemoryManager = new Microsoft.IO.RecyclableMemoryStreamManager()
對照對象池的思路,碼農說:“要有池。”就有了池。
碼農看池是全局的,就把池賦給了靜態字段。
碼農稱池為recyclableMemoryManager,這是頭一步。
唯一重要的就是下面這行代碼
this.smallPool = new ConcurrentStack<byte[]>();
………………只是簡單的建立了個棧來管理內存塊………………
對了,前面忘記補充說明下,為了充分管理內存的使用,RecyclableMemoryStream中並不是直接將MemoryStream
管理起來,而是另外實現了個MemoryStream,並改為由池來管理固定大小的byte[]
內存塊。
此處的smallPool
就是核心的內存塊池。
var ms = reusableMemoryStreamManager.GetStream()
接下來就是使用者向池申請資源了——才怪!
由於RecyclableMemoryStream的實現是基於內存塊管理的思路,故而創建一個新的RecyclableMemoryStream
實例不需要特別做什么。
ReusableMemoryStream.EnsureCapacity()
接下來就是使用者向池申請資源了。無論是向流中寫入數據,還是通過Capacity
或SetLength
方法
顯示設置流的長度,最終都需要確保流中有足夠的內存空間。廢話不說看源碼:
private void EnsureCapacity(int newCapacity)
{
while (this.Capacity < newCapacity)
{
blocks.Add((this.memoryManager.GetBlock()));
}
}
這里的blocks
是一個List<byte[]>
,用來存儲已分配到的內存塊。memoryManager
是RecyclableMemoryStreamManager
類型的對象池——
也就是最開始我們創建的recyclableMemoryManager
啦。
RecyclableMemoryStreamManager.GetBlock()
前面EnsureCapacity
方法的關鍵就是調用GetBlock
方法獲取內存塊——也就是對象池分配可用的對象。繼續廢話不說上代碼
internal byte[] GetBlock()
{
byte[] block;
if (!this.smallPool.TryPop(out block))
{
// We'll add this back to the pool when the stream is disposed
// (unless our free pool is too large)
block = new byte[this.BlockSize];
}
return block;
}
別說我偷懶,如此簡單的代碼還需要解釋么?
ReusableMemoryStream.Dispose()
上面偷懶我認了,接下來我堅決不偷懶——我要貼兩塊!
使用者用完后將對象還回池中,通常需要用戶釋放資源——.Net下通常意味着實現IDisposable
接口。
ReusableMemoryStream的Dispose
方法寫的比較完整,如果對Dispose
方法的正確姿勢沒有研究的話推薦看看。
但是話說回來,咱們現在討論的是對象池,所以關鍵的代碼只有一行:
this.memoryManager.ReturnBlocks(this.blocks);
而RecyclableMemoryStreamManager的ReturnBlocks
方法關鍵代碼如下:
本來想寫篇關於System.Collections.Immutable中提供的ImmutableList里一些實現細節來着,
結果一時想不起來源碼在哪里——為什么會變成這樣呢……第一次有了想寫分析的源碼,又有了寫博客的時間。
兩件快樂事情重合在一起。而這兩份快樂,又給我帶來更多的快樂。得到的,本該是像夢境一般幸福的時間……
但是,為什么,會變成這樣呢……還好順路看到MS開源的一個基於內存池的MemoryStream替代實現,看起來用這個水一篇文章妥妥的。
ps: 雖然在標題上扯了.net,不過說實話除了代碼是用c#外,我自己也想不出來其中的技術概念與.net有幾毛錢關系。如果願意的話,
絕大部分語言都可以實現來着。
## var ms = new MemoryStream()
在討論對象池之前,咱們先來看一段簡單的代碼。
```c#
var ms = new MemoryStream();
DataContractSerializer serializer = new DataContractSerializer(typeof(Model));
serializer.WriteObject(ms, model);
這段代碼采用DataContractSerializer序列化一個類型為Model的對象,沒有什么特殊的技巧可言。大部分情況下也沒必要去折騰這塊代碼。
不過,在實際系統中,此處還是存在着一個不大不小的問題——每次運行這個代碼段都會新建一個MemoryStream,
然后往這個MemoryStream中寫入字節流。
看上去MemoryStream非常神奇,它是一個基於內存,可以不斷寫入(只要還有內存)的流。不過如果查看
源碼,就會發現它實際上還是基於byte[]
實現的,每次擴展容量時,都新建一個足夠大byte[]
:
if (_expandable && value != _capacity) {
if (value > 0) {
byte[] newBuffer = new byte[value];
if (_length > 0) Buffer.InternalBlockCopy(_buffer, 0, newBuffer, 0, _length);
_buffer = newBuffer;
}
else {
_buffer = null;
}
_capacity = value;
}
別的不說,默默新建的那一堆byte[]
對GC產生的壓力,甚至有可能觸發過多的gen2 gc,
對提高系統的吞吐量都是非常不利的。最好能重復利用已經分配出來的內存空間,讓這些byte[]
活的越久越好。於是我們就得把用完的MemoryStream回收回來,
洗洗更健康再用。
對象池
其實對象池的基本思路非常簡單,無非分為以下幾步:
- 創建池
- 使用者向池申請資源
- 池分配可用的對象(如果沒有現成的,則創建個新對象)
- 使用者用完后將對象還回池中
無論是ADO.NET中的連接池,還是ThreadPool線程池,抑或接下來要說到的RecyclableMemoryStream內存池,都離不開這個思路。
下載源碼
由於原版的RecyclableMemoryStream中的功能完整,不太適合講解對象池的原理之用,故特地裁剪了個版本出來——雖然也能用,不過在實際項目中還是推薦用nuget安裝完整版的。
簡化版請點此處
static RecyclableMemoryStreamManager recyclableMemoryManager = new Microsoft.IO.RecyclableMemoryStreamManager()
對照對象池的思路,碼農說:“要有池。”就有了池。
碼農看池是全局的,就把池賦給了靜態字段。
碼農稱池為recyclableMemoryManager,這是頭一步。
唯一重要的就是下面這行代碼
this.smallPool = new ConcurrentStack<byte[]>();
………………只是簡單的建立了個棧來管理內存塊………………
對了,前面忘記補充說明下,為了充分管理內存的使用,RecyclableMemoryStream中並不是直接將MemoryStream管理起來,而是另外實現了個MemoryStream,並改為由池來管理固定大小的byte[]
內存塊。此處的smallPool
就是核心的內存塊池。
var ms = reusableMemoryStreamManager.GetStream()
接下來就是使用者向池申請資源了——才怪!
由於RecyclableMemoryStream的實現是基於內存塊管理的思路,故而創建一個新的RecyclableMemoryStream
實例時不需要特別做什么。
ReusableMemoryStream.EnsureCapacity()
接下來就是使用者向池申請資源了。無論是向流中寫入數據,還是通過Capacity
或SetLength
方法顯式設置流的長度,最終都需要確保流中有足夠的內存空間。廢話不說看源碼:
private void EnsureCapacity(int newCapacity)
{
while (this.Capacity < newCapacity)
{
blocks.Add((this.memoryManager.GetBlock()));
}
}
這里的blocks
是一個List<byte[]>
,用來存儲已分配到的內存塊。memoryManager
是RecyclableMemoryStreamManager
類型的對象池——
也就是最開始我們創建的recyclableMemoryManager
啦。
RecyclableMemoryStreamManager.GetBlock()
前面EnsureCapacity
方法的關鍵就是調用GetBlock
方法獲取內存塊——也就是對象池分配可用的對象。繼續廢話不說上代碼
internal byte[] GetBlock()
{
byte[] block;
if (!this.smallPool.TryPop(out block))
{
// We'll add this back to the pool when the stream is disposed
// (unless our free pool is too large)
block = new byte[this.BlockSize];
}
return block;
}
別說我偷懶,如此簡單的代碼還需要解釋么?
ReusableMemoryStream.Dispose()
上面偷懶我認了,接下來我堅決不偷懶——我要貼兩塊!
使用者用完后將對象還回池中,通常需要用戶釋放資源——.Net下通常意味着實現IDisposable
接口。
ReusableMemoryStream的Dispose
方法寫的比較完整,如果對Dispose
方法的正確姿勢沒有研究的話推薦看看。但是話說回來,咱們現在討論的是對象池,所以關鍵的代碼只有一行:
this.memoryManager.ReturnBlocks(this.blocks);
而RecyclableMemoryStreamManager的ReturnBlocks
方法關鍵代碼如下:
foreach (var block in blocks)
{
this.smallPool.Push(block);
}
到這里,內存塊又回歸內存池之海了(又是池又是海的……你要理解!理解!)。
結束語
只要理解了對象池技術的原理,就會發現這個技術一點都不復雜——雖然實際工程中可能還需要編寫大量的代碼。其實很多聽上去玄乎的技術,解釋開了也不過如此。
Adiós