Pooling(https://github.com/inversionhourglass/Pooling),編譯時對象池組件,在編譯時將指定類型的new
操作替換為對象池操作,簡化編碼過程,無需開發(fā)人員手動編寫對象池操作代碼。同時提供了完全無侵入式的解決方案,可用作臨時性能優(yōu)化的解決方案和老久項目性能優(yōu)化的解決方案等。
快速開始
引用Pooling.Fody
dotnet add package Pooling.Fody
確保FodyWeavers.xml
文件中已配置Pooling,如果當前項目沒有FodyWeavers.xml
文件,可以直接編譯項目,會自動生成FodyWeavers.xml
文件:
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Pooling />
</Weavers>
public class TestItem : IPoolItem
{
public int Value { get; set; }
public bool TryReset()
{
return true;
}
}
public class Test
{
public void M()
{
var random = new Random();
var item = new TestItem();
item.Value = random.Next();
Console.WriteLine(item.Value);
}
}
public class Test
{
public void M()
{
TestItem item = null;
try
{
var random = new Random();
item = Pool<TestItem>.Get();
item.Value = random.Next();
Console.WriteLine(item.Value);
}
finally
{
if (item != null)
{
Pool<TestItem>.Return(item);
}
}
}
}
IPoolItem
正如快速開始中的代碼所示,實現了IPoolItem
接口的類型便是一個池化類型,在編譯時Pooling會將其new操作替換為對象池操作,并在finally塊中將池化對象實例返還到對象池中。IPoolItem
僅有一個TryReset
方法,該方法用于在對象返回對象池時進行狀態(tài)重置,該方法返回false時表示狀態(tài)重置失敗,此時該對象將會被丟棄。
PoolingExclusiveAttribute
默認情況下,實現IPoolItem
的池化類型會在所有方法中進行池化操作,但有時候我們可能希望該池化類型在部分類型中不進行池化操作,比如我們可能會創(chuàng)建一些池化類型的管理類型或者Builder類型,此時在池化類型上應用PoolingExclusiveAttribute
便可指定該池化類型不在某些類型/方法中進行池化操作。
[PoolingExclusive(Types = [typeof(TestItemBuilder)], Pattern = "execution(* TestItemManager.*(..))")]
public class TestItem : IPoolItem
{
public bool TryReset() => true;
}
public class TestItemBuilder
{
private readonly TestItem _item;
private TestItemBuilder()
{
_item = new TestItem();
}
public static TestItemBuilder Create() => new TestItemBuilder();
public TestItemBuilder SetXxx()
{
return this;
}
public TestItem Build()
{
return _item;
}
}
public class TestItemManager
{
private TestItem? _cacheItem;
public void Execute()
{
var item = _cacheItem ?? new TestItem();
}
}
如上代碼所示,PoolingExclusiveAttribute
有兩個屬性Types
和Pattern
。Types
為Type
類型數組,當前池化類型不會在數組中的類型的方法中進行池化操作;Pattern
為string
類型AspectN表達式,可以細致的匹配到具體的方法(AspectN表達式格式詳見:https://github.com/inversionhourglass/Shared.Cecil.AspectN/blob/master/README.md ),當前池化類型不會在被匹配到的方法中進行池化操作。兩個屬性可以使用其中一個,也可以同時使用,同時使用時將排除兩個屬性匹配到的所有類型/方法。
NonPooledAttribute
前面介紹了可以通過PoolingExclusiveAttribute
指定當前池化對象在某些類型/方法中不進行池化操作,但由于PoolingExclusiveAttribute
需要直接應用到池化類型上,所以如果你使用了第三方類庫中的池化類型,此時你無法直接將PoolingExclusiveAttribute
應用到該池化類型上。針對此類情況,可以使用NonPooledAttribute
表明當前方法不進行池化操作。
public class TestItem1 : IPoolItem
{
public bool TryReset() => true;
}
public class TestItem2 : IPoolItem
{
public bool TryReset() => true;
}
public class TestItem3 : IPoolItem
{
public bool TryReset() => true;
}
public class Test
{
[NonPooled]
public void M()
{
var item1 = new TestItem1();
var item2 = new TestItem2();
var item3 = new TestItem3();
}
}
有的時候你可能并不是希望方法里所有的池化類型都不進行池化操作,此時可以通過NonPooledAttribute
的兩個屬性Types
和Pattern
指定不可進行池化操作的池化類型。Types
為Type
類型數組,數組中的所有類型在當前方法中均不可進行池化操作;Pattern
為string
類型AspectN類型表達式,所有匹配的類型在當前方法中均不可進行池化操作。
public class Test
{
[NonPooled(Types = [typeof(TestItem1)], Pattern = "*..TestItem3")]
public void M()
{
var item1 = new TestItem1();
var item2 = new TestItem2();
var item3 = new TestItem3();
}
}
AspectN類型表達式靈活多變,支持邏輯非操作符!
,所以可以很方便的使用AspectN類型表達式僅允許某一個類型,比如上面的示例可以簡單改為[NonPooled(Pattern = "!TestItem2")]
,更多AspectN表達式說明,詳見:https://github.com/inversionhourglass/Shared.Cecil.AspectN/blob/master/README.md 。
NonPooledAttribute
不僅可以應用于方法層級,還可以應用于類型和程序集。應用于類等同于應用到類的所有方法上(包括屬性和構造方法),應用于程序集等同于應用到當前程序集的所有方法上(包括屬性和構造方法),另外如果在應用到程序集時沒有指定Types
和Pattern
兩個屬性,那么就等同于當前程序集禁用Pooling。
無侵入式池化操作
看了前面的內容再看看標題,你可能就在嘀咕“這是哪門子無侵入式,這不純純標題黨”?,F在,標題的部分來了。Pooling提供了無侵入式的接入方式,適用于臨時性能優(yōu)化和老久項目改造,不需要實現IPoolItem
接口,通過配置即可指定池化類型。
假設目前有如下代碼:
namespace A.B.C;
public class Item1
{
public object? GetAndDelete() => null;
}
public class Item2
{
public bool Clear() => true;
}
public class Item3 { }
public class Test
{
public static void M1()
{
var item1 = new Item1();
var item2 = new Item2();
var item3 = new Item3();
Console.WriteLine($"{item1}, {item2}, {item3}");
}
public static async ValueTask M2()
{
var item1 = new Item1();
var item2 = new Item2();
await Task.Yield();
var item3 = new Item3();
Console.WriteLine($"{item1}, {item2}, {item3}");
}
}
項目在引用Pooling.Fody
后,編譯項目時項目文件夾下會生成一個FodyWeavers.xml
文件,我們按下面的示例修改Pooling
節(jié)點:
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Pooling>
<Items>
<Item pattern="A.B.C.Item1.GetAndDelete" />
<Item pattern="Item2.Clear" inspect="execution(* Test.M1(..))" />
<Item stateless="*..Item3" not-inspect="method(* Test.M2())" />
</Items>
</Pooling>
</Weavers>
上面的配置中,每一個Item
節(jié)點匹配一個池化類型,上面的配置中展示了全部的四個屬性,它們的含義分別是:
- pattern: AspectN類型+方法表達式。匹配到的類型為池化類型,匹配到的方法為狀態(tài)重置方法(等同于IPoolItem的TryReset方法)。需要注意的是,重置方法必須是無參的。
- stateless: AspectN類型表達式。匹配到的類型為池化類型,該類型為無狀態(tài)類型,不需要重置操作即可回到對象池中。
- inspect: AspectN表達式。
pattern
和stateless
匹配到的池化類型,只有在該表達式匹配到的方法中才會進行池化操作。當該配置缺省時表示匹配當前程序集的所有方法。 - not-inspect: AspectN表達式。
pattern
和stateless
匹配到的池化類型不會在該表達式匹配到的方法中進行池化操作。當該配置缺省時表示不排除任何方法。最終池化類型能夠進行池化操作的方法集合為inspect
集合與not-inspect
集合的差集。
那么通過上面的配置,Test
在編譯后的代碼為:
public class Test
{
public static void M1()
{
Item1 item1 = null;
Item2 item2 = null;
Item3 item3 = null;
try
{
item1 = Pool<Item1>.Get();
item2 = Pool<Item2>.Get();
item3 = Pool<Item3>.Get();
Console.WriteLine($"{item1}, {item2}, {item3}");
}
finally
{
if (item1 != null)
{
item1.GetAndDelete();
Pool<Item1>.Return(item1);
}
if (item2 != null)
{
if (item2.Clear())
{
Pool<Item2>.Return(item2);
}
}
if (item3 != null)
{
Pool<Item3>.Return(item3);
}
}
}
public static async ValueTask M2()
{
Item1 item1 = null;
try
{
item1 = Pool<Item1>.Get();
var item2 = new Item2();
await Task.Yield();
var item3 = new Item3();
Console.WriteLine($"{item1}, {item2}, {item3}");
}
finally
{
if (item1 != null)
{
item1.GetAndDelete();
Pool<Item1>.Return(item1);
}
}
}
}
細心的你可能注意到在M1
方法中,item1
和item2
在重置方法的調用上有所區(qū)別,這是因為Item2
的重置方法的返回值類型為bool
,Poolinng會將其結果作為是否重置成功的依據,對于void
或其他類型的返回值,Pooling將在方法成功返回后默認其重置成功。
零侵入式池化操作
看到這個標題是不是有點懵,剛介紹完無侵入式,怎么又來個零侵入式,它們有什么區(qū)別?
在上面介紹的無侵入式池化操作中,我們不需要改動任何C#代碼即可完成指定類型池化操作,但我們仍需要添加Pooling.Fody的NuGet依賴,并且需要修改FodyWeavers.xml進行配置,這仍然需要開發(fā)人員手動操作完成。那如何讓開發(fā)人員完全不需要任何操作呢?答案也很簡單,就是將這一步放到CI流程或發(fā)布流程中完成。是的,零侵入是針對開發(fā)人員的,并不是真的什么都不需要做,而是將引用NuGet和配置FodyWeavers.xml的步驟延后到CI/發(fā)布流程中了。
優(yōu)勢是什么
類似于對象池這類型的優(yōu)化往往不是僅僅某一個項目需要優(yōu)化,這種優(yōu)化可能是普遍性的,那么此時相比一個項目一個項目的修改,統(tǒng)一的在CI流程/發(fā)布流程中配置是更為快速的選擇。另外在面對一些古董項目時,可能沒有人愿意去更改任何代碼,即使只是項目文件和FodyWeavers.xml配置文件,此時也可以通過修改CI/發(fā)布流程來完成。當然修改統(tǒng)一的CI/發(fā)布流程的影響面可能更廣,這里只是提供一種零侵入式的思路,具體情況還需要結合實際情況綜合考慮。
如何實現
最直接的方式就是在CI構建流程或發(fā)布流程中通過dotnet add package Pooling.Fody
為項目添加NuGet依賴,然后將預先配置好的FodyWeavers.xml復制到項目目錄下。但如果項目還引用了其他Fody插件,直接覆蓋原有的FodyWeavers.xml可能導致原有的插件無效。當然,你也可以復雜點通過腳本控制FodyWeavers.xml的內容,這里我推薦一個.NET CLI工具,Cli4Fody可以一步完成NuGet依賴和FodyWeavers.xml配置。
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Pooling>
<Items>
<Item pattern="A.B.C.Item1.GetAndDelete" />
<Item pattern="Item2.Clear" inspect="execution(* Test.M1(..))" />
<Item stateless="*..Item3" not-inspect="method(* Test.M2())" />
</Items>
</Pooling>
</Weavers>
上面的FodyWeavers.xml,使用Cli4Fody對應的命令為:
fody-cli MySolution.sln \
--addin Pooling -pv 0.1.0 \
-n Items:Item -a "pattern=A.B.C.Item1.GetAndDelete" \
-n Items:Item -a "pattern=Item2.Clear" -a "inspect=execution(* Test.M1(..))" \
-n Items:Item -a "stateless=*..Item3" -a "not-inspect=method(* Test.M2())"
Cli4Fody的優(yōu)勢是,NuGet引用和FodyWeavers.xml可以同時完成,并且Cli4Fody并不會修改或刪除FodyWeavers.xml中其他Fody插件的配置。更多Cli4Fody相關配置,詳見:https://github.com/inversionhourglass/Cli4Fody
Rougamo零侵入式優(yōu)化案例
肉夾饃(Rougamo),一款靜態(tài)代碼編織的AOP組件。肉夾饃在2.2.0版本中新增了結構體支持,可以通過結構體優(yōu)化GC。但結構體的使用沒有類方便,不可繼承父類只能實現接口,所以很多MoAttribute
中的默認實現在定義結構體時需要重復實現?,F在,你可以使用Pooling通過對象池來優(yōu)化肉夾饃的GC。在這個示例中將使用Docker演示如何在Docker構建流程中使用Cli4Fody完成零侵入式池化操作:
目錄結構:
.
├── Lib
│ └── Lib.csproj
│ └── TestAttribute.cs
└── RougamoPoolingConsoleApp
└── BenchmarkTest.cs
└── Dockerfile
└── RougamoPoolingConsoleApp.csproj
└── Program.cs
該測試項目在BenchmarkTest.cs
里面定義了兩個空的測試方法M
和N
,兩個方法都應用了TestAttribute
。本次測試將在Docker的構建步驟中使用Cli4Fody為項目增加Pooling.Fody依賴并將TestAttribute
配置為池化類型,同時設置其只能在TestAttribute.M
方法中進行池化,然后通過Benchmark對比M
和N
的GC情況。
public class TestAttribute : MoAttribute
{
private readonly byte[] _occupy = new byte[1024];
}
public class BenchmarkTest
{
[Benchmark]
[Test]
public void M() { }
[Benchmark]
[Test]
public void N() { }
}
var config = ManualConfig.Create(DefaultConfig.Instance)
.AddDiagnoser(MemoryDiagnoser.Default);
var _ = BenchmarkRunner.Run<BenchmarkTest>(config);
Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /src
COPY . .
ENV PATH="$PATH:/root/.dotnet/tools"
RUN dotnet tool install -g Cli4Fody
RUN fody-cli DockerSample.sln --addin Rougamo -pv 4.0.4 --addin Pooling -pv 0.1.0 -n Items:Item -a "stateless=Rougamo.IMo+" -a "inspect=method(* RougamoPoolingConsoleApp.BenchmarkTest.M(..))"
RUN dotnet restore
RUN dotnet publish "./RougamoPoolingConsoleApp/RougamoPoolingConsoleApp.csproj" -c Release -o /src/bin/publish
WORKDIR /src/bin/publish
ENTRYPOINT ["dotnet", "RougamoPoolingConsoleApp.dll"]
通過Cli4Fody最終BenchmarkTest.M
中織入的TestAttribute
進行了池化操作,而BenchmarkTest.N
中織入的TestAttribute
沒有進行池化操作,最終Benchmark結果如下:
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
|------- |---------:|--------:|---------:|-------:|-------:|----------:|
| M | 188.7 ns | 3.81 ns | 6.67 ns | 0.0210 | - | 264 B |
| N | 195.5 ns | 4.09 ns | 11.74 ns | 0.1090 | 0.0002 | 1368 B |
完整示例代碼保存在:https://github.com/inversionhourglass/Pooling/tree/master/samples/DockerSample
在這個示例中,通過在Docker的構建步驟中使用Cli4Fody完成了對Rougamo的對象池優(yōu)化,整個過程對開發(fā)時完全無感零侵入的。如果你準備用這種方法對Rougamo進行對象池優(yōu)化,需要注意的是當前示例中的切面類型TestAttribute
是無狀態(tài)的,所以你需要跟開發(fā)確認所有定義的切面類型都是無狀態(tài)的,對于有狀態(tài)的切面類型,你需要定義重置方法并在定義Item節(jié)點時使用pattern屬性而不是stateless屬性。
在這個示例中還有一點你可能沒有注意,只有Lib項目引用了Rougamo.Fody,RougamoPoolingConsoleApp項目并沒有引用Rougamo.Fody,默認情況下應用到BenchmarkTest
的TestAttribute
應該是不會生效的,但我這個例子中卻生效了。這是因為在使用Cli4Fody時還指定了Rougamo的相關參數,Cli4Fody會為RougamoPoolingConsoleApp添加了Rougamo.Fody引用,所以Cli4Fody也可用于避免遺漏項目隊Fody插件的直接依賴,更多Cli4Fody的內容詳見:https://github.com/inversionhourglass/Cli4Fody
配置項
在無侵入式池化操作中介紹了Items
節(jié)點配置,除了Items
配置項Pooling還提供了其他配置項,下面是完整配置示例:
<Pooling enabled="true" composite-accessibility="false">
<Inspects>
<Inspect>any_aspectn_pattern</Inspect>
<Inspect>any_aspectn_pattern</Inspect>
</Inspects>
<NotInspects>
<NotInspect>any_aspectn_pattern</NotInspect>
<NotInspect>any_aspectn_pattern</NotInspect>
</NotInspects>
<Items>
<Item pattern="method_name_pattern" stateless="type_pattern" inspect="any_aspectn_pattern" not-inspect="any_aspectn_pattern" />
<Item pattern="method_name_pattern" stateless="type_pattern" inspect="any_aspectn_pattern" not-inspect="any_aspectn_pattern" />
</Items>
</Pooling>
節(jié)點路徑 | 屬性名稱 | 用途 |
---|
/Pooling | enabled | 是否啟用Pooling |
/Pooling | composite-accessibility | AspectN是否使用類+方法綜合可訪問性進行匹配。默認僅按方法可訪問性進行匹配,比如類的可訪問性為internal,方法的可訪問性為public,那么默認情況下該方法的可訪問性認定為public,將該配置設置為true后,該方法的可訪問性認定為internal |
/Pooling/Inspects/Inspect | [節(jié)點值] | AspectN表達式。 全局篩選器,只有被該表達式匹配的方法才會檢查內部是否使用到池化類型并進行池化操作替換。即使是實現了IPoolItem 的池化類型也會受限于該配置。 該節(jié)點可配置多條,匹配的方法集合為多條配置的并集。 該節(jié)點缺省時表示匹配當前程序集所有方法。 最終的方法集合是該節(jié)點配置匹配的集合與 /Pooling/NotInspects 配置匹配的集合的差集。 |
/Pooling/NotInspects/NotInspect | [節(jié)點值] | AspectN表達式。 全局篩選器,被該表達式匹配的方法的內部不會進行池化操作替換。即使是實現了IPoolItem 的池化類型也會受限于該配置。 該節(jié)點可配置多條,匹配的方法集合為多條配置的并集。 該節(jié)點缺省時表示不排除任何方法。 最終的方法集合是 /Pooling/Inspects 配置匹配的集合與該節(jié)點配置匹配的集合的差集。 |
/Pooling/Items/Item | pattern | AspectN類型+方法名表達式。 匹配的類型會作為池化類型,匹配的方法會作為重置方法。 重置方法必須是無參方法,如果方法返回值類型為bool ,返回值還會被作為是否重置成功的依據。 該屬性與stateless 屬性僅可二選一。 |
/Pooling/Items/Item | stateless | AspectN類型表達式。 匹配的類型會作為池化類型,該類型為無狀態(tài)類型,在回到對象池之前不需要進行重置。 該屬性與pattern 僅可二選一。 |
/Pooling/Items/Item | inspect | AspectN表達式。
pattern 和stateless 匹配到的池化類型,只有在該表達式匹配到的方法中才會進行池化操作。 當該配置缺省時表示匹配當前程序集的所有方法。 當前池化類型最終能夠應用的方法集合為該配置匹配的方法集合與not-inspect 配置匹配的方法集合的差集。 |
/Pooling/Items/Item | not-inspect | AspectN表達式。
pattern 和stateless 匹配到的池化類型不會在該表達式匹配到的方法中進行池化操作。 當該配置缺省時表示不排除任何方法。 當前池化類型最終能夠應用的方法集合為inspect 配置匹配的方法集合與該配置匹配的方法集合的差集。 |
可以看到配置中大量使用了AspectN表達式,了解更多AspectN表達式的用法詳見:https://github.com/inversionhourglass/Shared.Cecil.AspectN/blob/master/README.md
另外需要注意的是,程序集中的所有方法就像是內存,而AspectN就像指針,通過指針操作內存時需格外小心。將預期外的類型匹配為池化類型可能會導致同一個對象實例被并發(fā)的使用,所以在使用AspectN表達式時盡量使用精確匹配,避免使用模糊匹配。
對象池配置
對象池最大對象持有數量
每個池化類型的對象池最大持有對象數量為邏輯處理器數量乘以2Environment.ProcessorCount * 2
,有兩種方式可以修改這一默認設置。
通過代碼指定
通過Pool.GenericMaximumRetained
可以設置所有池化類型的對象池最大對象持有數量,通過Pool<T>.MaximumRetained
可以設置指定池化類型的對象池最大對象持有數量。后者優(yōu)先級高于前者。
通過環(huán)境變量指定
在應用啟動時指定環(huán)境變量可以修改對象池最大持有對象數量,NET_POOLING_MAX_RETAIN
用于設置所有池化類型的對象池最大對象持有數量,NET_POOLING_MAX_RETAIN_{PoolItemFullName}
用于設置指定池化類型的對象池最大對象持有數量,其中{PoolItemFullName}
為池化類型的全名稱(命名空間.類名),需要注意的是,需要將全名稱中的.
替換為_
,比如NET_POOLING_MAX_RETAIN_System_Text_StringBuilder
。環(huán)境變量的優(yōu)先級高于代碼指定,推薦使用環(huán)境變量進行控制,更為靈活。
自定義對象池
我們知道官方有一個對象池類庫Microsoft.Extensions.ObjectPool
,Pooling沒有直接引用這個類庫而選擇自建對象池,是因為Pooling作為編譯時組件,對方法的調用都是通過IL直接織入的,如果引用三方類庫,并且三方類庫在后續(xù)的更新對方法簽名有所修改,那么可能會在運行時拋出MethodNotFoundException
,所以盡量減少三方依賴是編譯時組件最好的選擇。
有的朋友可能會擔心自建對象池的性能問題,可以放心的是Pooling對象池的實現是從Microsoft.Extensions.ObjectPool
拷貝而來,同時精簡了ObjectPoolProvider
, PooledObjectPolicy
等元素,保持最精簡的默認對象池實現。同時,Pooling支持自定義對象池,實現IPool
接口定義通用對象池,實現IPool<T>
接口定義特定池化類型的對象池。下面簡單演示如何通過自定義對象池將對象池實現換為Microsoft.Extensions.ObjectPool
:
public class MicrosoftPool : IPool
{
private static readonly ConcurrentDictionary<Type, object> _Pools = [];
public T Get<T>() where T : class, new()
{
return GetPool<T>().Get();
}
public void Return<T>(T value) where T : class, new()
{
GetPool<T>().Return(value);
}
private ObjectPool<T> GetPool<T>() where T : class, new()
{
return (ObjectPool<T>)_Pools.GetOrAdd(typeof(T), t =>
{
var provider = new DefaultObjectPoolProvider();
var policy = new DefaultPooledObjectPolicy<T>();
return provider.Create(policy);
});
}
}
public class SpecificalMicrosoftPool<T> : IPool<T> where T : class, new()
{
private readonly ObjectPool<T> _pool;
public SpecificalMicrosoftPool()
{
var provider = new DefaultObjectPoolProvider();
var policy = new DefaultPooledObjectPolicy<T>();
_pool = provider.Create(policy);
}
public T Get()
{
return _pool.Get();
}
public void Return(T value)
{
_pool.Return(value);
}
}
Pool.Set(new MicrosoftPool());
Pool<Xyz>.Set(new SpecificalMicrosoftPool<Xyz>());
不僅僅用作對象池
雖然Pooling的意圖是簡化對象池操作和無侵入式的項目改造優(yōu)化,但得益于Pooling的實現方式以及提供的自定義對象池功能,你可以使用Pooling完成的事情不僅僅是對象池,Pooling的實現相當于在所有無參構造方法調用的地方埋入了一個探針,你可以在這里做任何事情,下面簡單舉幾個例子。
單例
public class SingletonPool<T> : IPool<T> where T : class, new()
{
private readonly T _value = new();
public T Get() => _value;
public void Return(T value) { }
}
Pool<ConcurrentDictionary<Type, object>>.Set(new SingletonPool<ConcurrentDictionary<Type, object>>());
通過上面的改動,你成功的讓所有的ConcurrentDictionary<Type, object>>
共享一個實例。
控制信號量
public class SemaphorePool<T> : IPool<T> where T : class, new()
{
private readonly Semaphore _semaphore = new(3, 3);
private readonly DefaultPool<T> _pool = new();
public T Get()
{
if (!_semaphore.WaitOne(100)) return null;
return _pool.Get();
}
public void Return(T value)
{
_pool.Return(value);
_semaphore.Release();
}
}
Pool<Connection>.Set(new SemaphorePool<Connection>());
在這個例子中使用信號量對象池控制Connection
的數量,對于一些限流場景非常適用。
線程單例
public class ThreadLocalPool<T> : IPool<T> where T : class, new()
{
private readonly ThreadLocal<T> _random = new(() => new());
public T Get() => _random.Value!;
public void Return(T value) { }
}
Pool<Random>.Set(new ThreadLocalPool<Random>());
當你想通過單例來減少GC壓力但對象又不是線程安全的,此時便可以ThreadLocal
實現線程內單例。
額外的初始化
public class ServiceSetupPool : IPool<Service1>
{
public Service1 Get()
{
var service1 = new Service1();
var service2 = PinnedScope.ScopedServices?.GetService<Service2>();
service1.Service2 = service2;
return service1;
}
public void Return(Service1 value) { }
}
public class Service2 { }
[PoolingExclusive(Types = [typeof(ServiceSetupPool)])]
public class Service1 : IPoolItem
{
public Service2? Service2 { get; set; }
public bool TryReset() => true;
}
Pool<Service1>.Set(new ServiceSetupPool());
在這個例子中使用Pooling結合DependencyInjection.StaticAccessor完成屬性注入,使用相同方式可以完成其他初始化操作。
發(fā)揮想象力
前面的這些例子可能不一定實用,這些例子的主要目的是啟發(fā)大家開拓思路,理解Pooling的基本實現原理是將臨時變量的new操作替換為對象池操作,理解自定義對象池的可擴展性。也許你現在用不上Pooling,但未來的某個需求場景下,你可能可以用Pooling快速實現而不需要大量改動代碼。
注意事項
不要在池化類型的構造方法中執(zhí)行復用時的初始化操作
從對象池中獲取的對象可能是復用的對象,被復用的對象是不會再次執(zhí)行構造方法的,所以如果你有一些初始化操作希望每次復用時都執(zhí)行,那么你應該將該操作獨立到一個方法中并在new操作后調用而不應該放在構造方法中
public class Connection : IPoolItem
{
private readonly Socket _socket;
public Connection()
{
_socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_socket.Connect("127.0.0.1", 8888);
}
public void Write(string message)
{
}
public bool TryReset()
{
_socket.Disconnect(true);
return true;
}
}
var connection = new Connection();
connection.Write("message");
public class Connection : IPoolItem
{
private readonly Socket _socket;
public Connection()
{
_socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
public void Connect()
{
_socket.Connect("127.0.0.1", 8888);
}
public void Write(string message)
{
}
public bool TryReset()
{
_socket.Disconnect(true);
return true;
}
}
var connection = new Connection();
connection.Connect();
connection.Write("message");
僅支持將無參構造方法的new操作替換為對象池操作
由于復用的對象無法再次執(zhí)行構造方法,所以構造參數對于池化對象毫無意義。如果希望通過構造參數完成一些初始化操作,可以將新建一個初始化方法接收這些參數并完成初始化,或通過屬性接收這些參數。
Pooling在編譯時會檢查new操作是否調用了無參構造方法,如果調用了有參構造方法,將不會將本次new操作替換為對象池操作。
注意不要將池化類型實例進行持久化保存
Pooling的對象池操作是方法級別的,也就是池化對象在當前方法中創(chuàng)建也在當前方法結束時釋放,不可將池化對象持久化到字段之中,否則會存在并發(fā)使用的風險。如果池化對象的聲明周期跨越了多個方法,那么你應該手動創(chuàng)建對象池并手動管理該對象。
Pooling在編譯時會進行簡單的持久化排查,對于排查出來的池化對象將不進行池化操作。但需要注意的是,這種排查僅可排查一些簡單的持久化操作,無法排查出復雜情況下的持久化操作,比如你在當前方法中調用另一個方法傳入了池化對象實例,然后在被調用方法中進行持久化操作。所以根本上還是需要你自己注意,避免將池化對象持久化保存。
需要編譯時進行對象池操作替換的程序集都需要引用Pooling.Fody
Pooling的原理是在編譯時檢查所有方法(也可以通過配置選擇部分方法)的MSIL,排查所有newobj操作完成對象池替換操作,觸發(fā)該操作是通過Fody添加了一個MSBuild任務完成的,而只有當前程序集直接引用了Fody才能夠完成添加MSBuild任務這一操作。Pooling.Fody通過一些配置使得直接引用Pooling.Fody也可完成添加MSBuild任務的操作。
多個Fody插件同時使用時的注意事項
當項目引用了一個Fody插件時,在編譯時會自動生成一個FodyWeavers.xml
文件,如果在FodyWeavers.xml
文件已存在的情況下再引用一個其他Fody插件,此時再編譯,新的插件將不會追加到FodyWeavers.xml
文件中,需要手動配置。同時在引用多個Fody插件時需要注意他們在FodyWeavers.xml
中的順序,FodyWeavers.xml
順序對應著插件執(zhí)行順序,部分Fody插件可能存在功能交叉,不同的順序可能產生不同的效果。
AspectN
在文章的最后再提一下AspectN,之前一直稱其為AspectJ-Like表達式,因為確實是參照AspectJ表達式的格式設計的,不過一直這么叫也不是辦法,現在按照慣例更名為AspectN表達式(搜了一下,.NET里面沒有這個名詞,應該不存在沖突)。AspectN最早起源于肉夾饃2.0,用于提供更加精確的切入點匹配,現在再次投入到Pooling中使用。
在使用Fody或直接使用Mono.Cecil開發(fā)MSBuild任務插件時,如何查找到需要修改的類型或方法永遠是首要任務。最常用的方式便是通過類型和方法上的Attribute元數據進行定位,但這樣做基本確定了必須要修改代碼來添加Attribute應用,這是侵入性的。AspectN提供了非侵入式的類型和方法匹配機制,字符串可承載的無窮信息給予了AspectN無限的精細化匹配可能。很多Fody插件都可以借助AspectN實現無侵入式代碼織入,比如ConfigureAwait.Fody,可以使用AspectN實現通過配置指定哪些類型或方法需要應用ConfigureAwait,哪些不需要。
AspectN不依賴于Fody,僅依賴于Mono.Cecil,如果你有在使用Fody或Mono.Cecil,或許可以嘗試一下AspectN(https://github.com/inversionhourglass/Shared.Cecil.AspectN)。AspectN是一個共享項目(Shared Project),沒有發(fā)布NuGet,也沒有依賴具體Mono.Cecil的版本,使用AspectN你需要將AspectN克隆到本地作為共享項目直接引用,如果你的項目使用git進行管理,那么推薦將AspectN作為一個submodule添加到你的倉庫中(可以參考Rougamo和Pooling)。
轉自https://www.cnblogs.com/nigture/p/18468831
該文章在 2024/10/16 9:43:48 編輯過