写点什么

.NET 无侵入式对象池解决方案

作者:EquatorCoco
  • 2024-10-16
    福建
  • 本文字数:13010 字

    阅读完需:约 43 分钟

Pooling,编译时对象池组件,在编译时将指定类型的new操作替换为对象池操作,简化编码过程,无需开发人员手动编写对象池操作代码。同时提供了完全无侵入式的解决方案,可用作临时性能优化的解决方案和老久项目性能优化的解决方案等。


快速开始


引用 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 /> <!--确保存在Pooling节点--></Weavers>
复制代码


// 1. 需要池化的类型实现IPoolItem接口public class TestItem : IPoolItem{    public int Value { get; set; }
// 当对象返回对象池化时通过该方法进行重置实例状态 public bool TryReset() { return true; }}
// 2. 在任何地方使用new关键字创建该类型的对象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方法,该方法用于在对象返回对象池时进行状态重置,该方法返回 false 时表示状态重置失败,此时该对象将会被丢弃。


PoolingExclusiveAttribute


默认情况下,实现IPoolItem的池化类型会在所有方法中进行池化操作,但有时候我们可能希望该池化类型在部分类型中不进行池化操作,比如我们可能会创建一些池化类型的管理类型或者 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() { // 由于通过PoolingExclusive的Types属性排除了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() { // 由于通过PoolingExclusive的Pattern属性排除了TestItemManager下的所有方法,所以这里不会替换为对象池操作 var item = _cacheItem ?? new TestItem(); // ... }}
复制代码


如上代码所示,PoolingExclusiveAttribute有两个属性TypesPatternTypesType类型数组,当前池化类型不会在数组中的类型的方法中进行池化操作;Patternstring类型 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() { // 由于方法应用了NonPooledAttribute,以下三个new操作都不会替换为对象池操作 var item1 = new TestItem1(); var item2 = new TestItem2(); var item3 = new TestItem3(); }}
复制代码


有的时候你可能并不是希望方法里所有的池化类型都不进行池化操作,此时可以通过NonPooledAttribute的两个属性TypesPattern指定不可进行池化操作的池化类型。TypesType类型数组,数组中的所有类型在当前方法中均不可进行池化操作;Patternstring类型 AspectN 类型表达式,所有匹配的类型在当前方法中均不可进行池化操作。


public class Test{    [NonPooled(Types = [typeof(TestItem1)], Pattern = "*..TestItem3")]    public void M()    {        // TestItem1通过Types不允许进行池化操作,TestItem3通过Pattern不允许进行池化操作,仅TestItem2可进行池化操作        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不仅可以应用于方法层级,还可以应用于类型和程序集。应用于类等同于应用到类的所有方法上(包括属性和构造方法),应用于程序集等同于应用到当前程序集的所有方法上(包括属性和构造方法),另外如果在应用到程序集时没有指定TypesPattern两个属性,那么就等同于当前程序集禁用 Pooling。


无侵入式池化操作


看了前面的内容再看看标题,你可能就在嘀咕“这是哪门子无侵入式,这不纯纯标题党”。现在,标题的部分来了。Pooling 提供了无侵入式的接入方式,适用于临时性能优化和老久项目改造,不需要实现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节点:


<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节点匹配一个池化类型,上面的配置中展示了全部的四个属性,它们的含义分别是:


  • pattern: AspectN 类型+方法表达式。匹配到的类型为池化类型,匹配到的方法为状态重置方法(等同于 IPoolItem 的 TryReset 方法)。需要注意的是,重置方法必须是无参的。

  • stateless: AspectN 类型表达式。匹配到的类型为池化类型,该类型为无状态类型,不需要重置操作即可回到对象池中。

  • inspect: AspectN 表达式。patternstateless匹配到的池化类型,只有在该表达式匹配到的方法中才会进行池化操作。当该配置缺省时表示匹配当前程序集的所有方法。

  • not-inspect: AspectN 表达式。patternstateless匹配到的池化类型不会在该表达式匹配到的方法中进行池化操作。当该配置缺省时表示不排除任何方法。最终池化类型能够进行池化操作的方法集合为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方法中,item1item2在重置方法的调用上有所区别,这是因为Item2的重置方法的返回值类型为bool,Poolinng 会将其结果作为是否重置成功的依据,对于void或其他类型的返回值,Pooling 将在方法成功返回后默认其重置成功。


零侵入式池化操作


看到这个标题是不是有点懵,刚介绍完无侵入式,怎么又来个零侵入式,它们有什么区别?


在上面介绍的无侵入式池化操作中,我们不需要改动任何 C#代码即可完成指定类型池化操作,但我们仍需要添加 Pooling.Fody 的 NuGet 依赖,并且需要修改 FodyWeavers.xml 进行配置,这仍然需要开发人员手动操作完成。那如何让开发人员完全不需要任何操作呢?答案也很简单,就是将这一步放到 CI 流程或发布流程中完成。是的,零侵入是针对开发人员的,并不是真的什么都不需要做,而是将引用 NuGet 和配置 FodyWeavers.xml 的步骤延后到 CI/发布流程中了。


优势是什么


类似于对象池这类型的优化往往不是仅仅某一个项目需要优化,这种优化可能是普遍性的,那么此时相比一个项目一个项目的修改,统一的在 CI 流程/发布流程中配置是更为快速的选择。另外在面对一些古董项目时,可能没有人愿意去更改任何代码,即使只是项目文件和 FodyWeavers.xml 配置文件,此时也可以通过修改 CI/发布流程来完成。当然修改统一的 CI/发布流程的影响面可能更广,这里只是提供一种零侵入式的思路,具体情况还需要结合实际情况综合考虑。


如何实现


最直接的方式就是在 CI 构建流程或发布流程中通过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 的优势是,NuGet 引用和 FodyWeavers.xml 可以同时完成,并且 Cli4Fody 并不会修改或删除 FodyWeavers.xml 中其他 Fody 插件的配置。更多 Cli4Fody 相关配置,详见:https://github.com/inversionhourglass/Cli4Fody


Rougamo 零侵入式优化案例


肉夹馍(Rougamo),一款静态代码编织的 AOP 组件。肉夹馍在 2.2.0 版本中新增了结构体支持,可以通过结构体优化 GC。但结构体的使用没有类方便,不可继承父类只能实现接口,所以很多MoAttribute中的默认实现在定义结构体时需要重复实现。现在,你可以使用 Pooling 通过对象池来优化肉夹馍的 GC。在这个示例中将使用 Docker 演示如何在 Docker 构建流程中使用 Cli4Fody 完成零侵入式池化操作:


目录结构:


.├── Lib│   └── Lib.csproj                       # 依赖Rougamo.Fody│   └── TestAttribute.cs                 # 继承MoAttribute└── RougamoPoolingConsoleApp    └── BenchmarkTest.cs    └── Dockerfile    └── RougamoPoolingConsoleApp.csproj  # 引用Lib.csproj,没有任何Fody插件依赖    └── Program.cs
复制代码


该测试项目在BenchmarkTest.cs里面定义了两个空的测试方法MN,两个方法都应用了TestAttribute。本次测试将在 Docker 的构建步骤中使用 Cli4Fody 为项目增加 Pooling.Fody 依赖并将TestAttribute配置为池化类型,同时设置其只能在TestAttribute.M方法中进行池化,然后通过 Benchmark 对比MN的 GC 情况。


// TestAttributepublic class TestAttribute : MoAttribute{    // 为了让GC效果更明显,每个TestAttribute都将持有长度为1024的字节数组    private readonly byte[] _occupy = new byte[1024];}
// BenchmarkTestpublic class BenchmarkTest{ [Benchmark] [Test] public void M() { }
[Benchmark] [Test] public void N() { }}
// Programvar config = ManualConfig.Create(DefaultConfig.Instance) .AddDiagnoser(MemoryDiagnoser.Default);var _ = BenchmarkRunner.Run<BenchmarkTest>(config);
复制代码


Dockerfile


FROM mcr.microsoft.com/dotnet/sdk:8.0WORKDIR /srcCOPY . .
ENV PATH="$PATH:/root/.dotnet/tools"RUN dotnet tool install -g Cli4FodyRUN 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/publishENTRYPOINT ["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 的对象池优化,整个过程对开发时完全无感零侵入的。如果你准备用这种方法对 Rougamo 进行对象池优化,需要注意的是当前示例中的切面类型TestAttribute是无状态的,所以你需要跟开发确认所有定义的切面类型都是无状态的,对于有状态的切面类型,你需要定义重置方法并在定义 Item 节点时使用 pattern 属性而不是 stateless 属性。

在这个示例中还有一点你可能没有注意,只有 Lib 项目引用了 Rougamo.Fody,RougamoPoolingConsoleApp 项目并没有引用 Rougamo.Fody,默认情况下应用到BenchmarkTestTestAttribute应该是不会生效的,但我这个例子中却生效了。这是因为在使用 Cli4Fody 时还指定了 Rougamo 的相关参数,Cli4Fody 会为 RougamoPoolingConsoleApp 添加了 Rougamo.Fody 引用,所以 Cli4Fody 也可用于避免遗漏项目队 Fody 插件的直接依赖,更多 Cli4Fody 的内容详见:https://github.com/inversionhourglass/Cli4Fody

配置项


无侵入式池化操作中介绍了Items节点配置,除了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>
复制代码



可以看到配置中大量使用了 AspectN 表达式,了解更多 AspectN 表达式的用法详见:https://github.com/inversionhourglass/Shared.Cecil.AspectN/blob/master/README.md

另外需要注意的是,程序集中的所有方法就像是内存,而 AspectN 就像指针,通过指针操作内存时需格外小心。将预期外的类型匹配为池化类型可能会导致同一个对象实例被并发的使用,所以在使用 AspectN 表达式时尽量使用精确匹配,避免使用模糊匹配。


对象池配置


对象池最大对象持有数量


每个池化类型的对象池最大持有对象数量为逻辑处理器数量乘以 2Environment.ProcessorCount * 2,有两种方式可以修改这一默认设置。


1、通过代码指定

通过Pool.GenericMaximumRetained可以设置所有池化类型的对象池最大对象持有数量,通过Pool<T>.MaximumRetained可以设置指定池化类型的对象池最大对象持有数量。后者优先级高于前者。


2、通过环境变量指定

在应用启动时指定环境变量可以修改对象池最大持有对象数量,NET_POOLING_MAX_RETAIN用于设置所有池化类型的对象池最大对象持有数量,NET_POOLING_MAX_RETAIN_{PoolItemFullName}用于设置指定池化类型的对象池最大对象持有数量,其中{PoolItemFullName}为池化类型的全名称(命名空间.类名),需要注意的是,需要将全名称中的.替换为_,比如NET_POOLING_MAX_RETAIN_System_Text_StringBuilder。环境变量的优先级高于代码指定,推荐使用环境变量进行控制,更为灵活。


自定义对象池


我们知道官方有一个对象池类库Microsoft.Extensions.ObjectPool,Pooling 没有直接引用这个类库而选择自建对象池,是因为 Pooling 作为编译时组件,对方法的调用都是通过 IL 直接织入的,如果引用三方类库,并且三方类库在后续的更新对方法签名有所修改,那么可能会在运行时抛出MethodNotFoundException,所以尽量减少三方依赖是编译时组件最好的选择。


有的朋友可能会担心自建对象池的性能问题,可以放心的是 Pooling 对象池的实现是从Microsoft.Extensions.ObjectPool拷贝而来,同时精简了ObjectPoolProviderPooledObjectPolicy等元素,保持最精简的默认对象池实现。同时,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); }}
// 替换操作最好在Main入口直接完成,一旦对象池被使用就不再运行进行替换操作
// 替换通用对象池实现Pool.Set(new MicrosoftPool());// 替换特定类型对象池Pool<Xyz>.Set(new SpecificalMicrosoftPool<Xyz>());
复制代码


不仅仅用作对象池


虽然 Pooling 的意图是简化对象池操作和无侵入式的项目改造优化,但得益于 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>设置为池化类型// <Item stateless="System.Collections.Concurrent.ConcurrentDictionary&lt;System.Type, object&gt;" />
复制代码


通过上面的改动,你成功的让所有的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设置为池化类型// <Item stateless="X.Y.Z.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>());
// 通过配置,将Connection设置为池化类型// <Item stateless="System.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完成属性注入,使用相同方式可以完成其他初始化操作。


发挥想象力


前面的这些例子可能不一定实用,这些例子的主要目的是启发大家开拓思路,理解 Pooling 的基本实现原理是将临时变量的 new 操作替换为对象池操作,理解自定义对象池的可扩展性。也许你现在用不上 Pooling,但未来的某个需求场景下,你可能可以用 Pooling 快速实现而不需要大量改动代码。


注意事项


1、不要在池化类型的构造方法中执行复用时的初始化操作


从对象池中获取的对象可能是复用的对象,被复用的对象是不会再次执行构造方法的,所以如果你有一些初始化操作希望每次复用时都执行,那么你应该将该操作独立到一个方法中并在 new 操作后调用而不应该放在构造方法中


// 修改前池化对象定义public class Connection : IPoolItem{    private readonly Socket _socket;
public Connection() { _socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 不应该在这里Connect,应该将Connect操作单独独立为一个方法,然后再new操作后调用 _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");
复制代码


2、仅支持将无参构造方法的 new 操作替换为对象池操作


由于复用的对象无法再次执行构造方法,所以构造参数对于池化对象毫无意义。如果希望通过构造参数完成一些初始化操作,可以将新建一个初始化方法接收这些参数并完成初始化,或通过属性接收这些参数。

Pooling 在编译时会检查 new 操作是否调用了无参构造方法,如果调用了有参构造方法,将不会将本次 new 操作替换为对象池操作。


3、注意不要将池化类型实例进行持久化保存


Pooling 的对象池操作是方法级别的,也就是池化对象在当前方法中创建也在当前方法结束时释放,不可将池化对象持久化到字段之中,否则会存在并发使用的风险。如果池化对象的声明周期跨越了多个方法,那么你应该手动创建对象池并手动管理该对象。


Pooling 在编译时会进行简单的持久化排查,对于排查出来的池化对象将不进行池化操作。但需要注意的是,这种排查仅可排查一些简单的持久化操作,无法排查出复杂情况下的持久化操作,比如你在当前方法中调用另一个方法传入了池化对象实例,然后在被调用方法中进行持久化操作。所以根本上还是需要你自己注意,避免将池化对象持久化保存。


4、需要编译时进行对象池操作替换的程序集都需要引用 Pooling.Fody


Pooling 的原理是在编译时检查所有方法(也可以通过配置选择部分方法)的 MSIL,排查所有 newobj 操作完成对象池替换操作,触发该操作是通过 Fody 添加了一个 MSBuild 任务完成的,而只有当前程序集直接引用了 Fody 才能够完成添加 MSBuild 任务这一操作。Pooling.Fody 通过一些配置使得直接引用 Pooling.Fody 也可完成添加 MSBuild 任务的操作。


5、多个 Fody 插件同时使用时的注意事项


当项目引用了一个 Fody 插件时,在编译时会自动生成一个FodyWeavers.xml文件,如果在FodyWeavers.xml文件已存在的情况下再引用一个其他 Fody 插件,此时再编译,新的插件将不会追加到FodyWeavers.xml文件中,需要手动配置。同时在引用多个 Fody 插件时需要注意他们在FodyWeavers.xml中的顺序,FodyWeavers.xml顺序对应着插件执行顺序,部分 Fody 插件可能存在功能交叉,不同的顺序可能产生不同的效果。


AspectN


在文章的最后再提一下 AspectN,之前一直称其为 AspectJ-Like 表达式,因为确实是参照 AspectJ 表达式的格式设计的,不过一直这么叫也不是办法,现在按照惯例更名为 AspectN 表达式(搜了一下,.NET 里面没有这个名词,应该不存在冲突)。AspectN 最早起源于肉夹馍 2.0,用于提供更加精确的切入点匹配,现在再次投入到 Pooling 中使用。


在使用 Fody 或直接使用 Mono.Cecil 开发 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),没有发布 NuGet,也没有依赖具体 Mono.Cecil 的版本,使用 AspectN 你需要将 AspectN 克隆到本地作为共享项目直接引用,如果你的项目使用 git 进行管理,那么推荐将 AspectN 作为一个 submodule 添加到你的仓库中(可以参考RougamoPooling)。


文章转载自:nigture

原文链接:https://www.cnblogs.com/nigture/p/18468831

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
.NET无侵入式对象池解决方案_.net_EquatorCoco_InfoQ写作社区