通俗易懂,C#如何安全、高效地玩转任何种类的内存之Memory<T>(三)

本文深入探讨了.NET Core中Memory<T>的起源、特性和使用方法,它是为解决Span<T>无法驻留在堆上的问题而设计的,适用于异步方法、类字段等高级场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

我们都知道,.Net Core是微软推出的一个通用开发平台,它是跨平台和开源的,由一个.NET运行时、一组可重用的框架库、一组SDK工具和语言编译器组成,旨在让.Net developers可以更容易地编写高性能的服务应用程序和基于云的可伸缩服务,比如微服务、物联网、云原生等等;在这些场景下,对于内存的消耗往往十分敏感,也十分苛刻;为了解决这个棘手问题,同时释放应用开发人员的精力,让他们能够安心地使用Net Core,而不用担心这些应用场景下的性能问题,故从.NET Core 2.1开始引进了两个新的旗舰类型:Span<T>Memory<T> ,使用它们可以避免分配缓冲区和不必要的数据复制

前面已经对span做了详细地讲解,所以今天主题是Memory,同样以Why、What和How的方式缓缓道来 ,让你知其然,更知其所以然。

Memory<T>是Span的补充,它是为了解决Span无法驻留到堆上而诞生的,可以说Span是Memory的奠基,故在读这篇文章前,请先仔细品读前面两篇文章:

现在,作者就当你已经阅读了前面的博客,并明白了Span的本质(ref-like type)和秉性特点(stack-only)。

why - 为什么需要memory ?

span的局限性

  1. span只能存储到执行栈上,保障操作效率与数组一样高,并提供稳定的生命周期。
  2. span不能被装箱到堆上,避免栈撕裂问题。
  3. span不能用作泛型类型参数
  4. Span不能作为类的字段
  5. Span不能实现任何接口
  6. Span不能用于异步方法,因为无法跨越await边界,所有无法跨异步操作暂留。

下面来看一个例子:

async Task DoSomethingAsync(Span<byte> buffer) {// 这里编译器会提示报错,作为例子而已,请忽略。
    buffer[0] = 0;
    await Something(); // 异步方法会释放当前执行栈,那么Span也被回收了。
    buffer[0] = 1; // 这里buffer将无法继续。
}

备注:C#编译器和core运行时内部会强制验证Span的局限性,所以上面例子才会编译不过。

正是因为这些局限性,确保了更高效、安全的内存访问

也是因为这些局限性,无法用于需要将引用数据存储到堆上的一些高级应用场景,比如:异步方法、类字段、泛型参数、集合成员、lambda表达式、迭代器等

还是因为这些局限性,增加了span对于高层开发人员的复杂性

所以Memory<T>诞生了,作为span的补充,它就是目前的解决方案,没有之一,也是高层开发人员日后使用最普遍的类型。

what - memory是什么 ?

Span<T>一样,也是sliceable type,但它不是ref-like type,就是普通的C#结构体。这意味着,可以将它装箱到堆上、作为类的字段或异步方法的参数、保存到集合等等,对于高层开发人员非常友好,嘿嘿,并且当需要处理Memory底层缓冲区,即做同步处理时,直接调用它的Span属性,同时又获得了高效的索引能力。

备注:Memory<T>表示一段可读写的连续内存区域,ReadOnlyMemory表示一段只读的连续内存区域。

static async Task<uint> ChecksumReadAsync(Memory<byte> buffer, Stream stream)
{
    var bytesRead = await stream.ReadAsync(buffer);
    // 需要同步处理时,直接调用span属性。
    return SafeSum(buffer.Span.Slice(0, bytesRead));
    // 千万不要这样写,除非你想要先持久化分片数据到托管堆上,但这又无法使用Span<T>实现;其次Memory <T>是一个比Span<T>更大的结构体,切片往往相对较慢。
    //return SafeSum(buffer.Slice(0,bytesRead).Span());
}
static uint SafeSum(Span<byte> buffer)
{
    uint sum = 0;
    foreach (var t in buffer)
    {
        sum += t;
    }
    return sum;
}

Memory核心设计

public readonly struct Memory<T>
{
	private readonly object _object; //表示Memory能包裹的对象,EveryThing。
	private readonly int _index;
	private readonly int _length; 
	public Span<T> Span { get; } // 实际的内部缓冲区
}	

如前所述,Memory的目的是为了解决Span无法驻留到堆上的问题,也就是Memory代表的内存块并不会随方法执行栈的unwind而回收,也就是说它的内部缓冲区是有生命周期的,并不是短暂的,这就是为什么字段_object的类型被设计成object,而不是类型化为T[],就是为了通过传递IMemoryOwner来管理Span的生命周期,从而避免UAF(use-after-free)bug。

private static MemoryPool<byte> _memPool = MemoryPool<byte>.Shared;

public async Task UsageWithLifeAsync(int size)
{
    using (var owner = _memPool.Rent(size)) // 从池里租借一块IMemoryOwner包裹的内存。
    {
        await DoSomethingAsync(owner.Memory); // 把实际的内存借给异步方法使用。
    } // 作用域结束,存储的Memory<T>被回收,这里是返回内存池,有借有还,再借不难,嘿嘿。
}
// 不用担心span会随着方法执行栈unwind而回收
async Task DoSomethingAsync(Memory<byte> buffer) {
    buffer.Span[0] = 0; // 没问题
    await Something(); // 跨越await边界。
    buffer.Span[0] = 1; // 没问题
}

IMemoryOwner,顾名思义,Memory<T>拥有者,通过属性Memory来表示,如下:

public interface IMemoryOwner<T> : IDisposable
{
    Memory<T> Memory { get; }
}

所以,可以使用IMemoryOwner来转移Memory<T>内部缓冲区的所有权,从而让开发人员不必管理缓冲区。

Memory<T>内部缓冲区生命周期的管理实际上非常复杂,用法如上所诉,可以简单地理解Memory<T>通过工厂来管理Span<T>,感兴趣的同学可以自行下去研究。

How - 如何运用memory ?

如前所述, Memory<T>其实就是Span<T>heap-able类型,故它的API和span基本相同,如下:

public Memory(T[] array);
public Memory(T[] array, int start, int length);
public Memory<T> Slice(int start);// 支持sliceable
public bool TryCopyTo(Memory<T> destination);

不同的是Memory<T>有两个独一无二的API,如下:

public MemoryHandle Pin(); // 钉住_object的内存地址,即告知垃圾回收器不要回收它,我们自己管理内存。
public System.Span<T> Span { get; }// 当_object字段为数组时,提供快速索引的能力。

Span<T>一样,通常Memory<T>都是包裹数组、字符串,用法也基本相同,只是应用场景不一样而已。

Memory<T>的使用指南

  • 同步方法应该接受Span参数,异步方法应该接受Memory参数。
  • Memory<T>作为参数无返回值的同步方法,方法结束后,不应该再使用它。
  • Memory<T>作为参数返回Task的异步方法,方法结束后,不应该再使用它。
  • 同一Memory<T>实例不能同时被多个消费者使用。

所以啊,千万不要将好东西用错地方了,聪明反被聪明误,最后,弄巧成拙,嘿嘿。

总结

综上所述,和Span<T>一样,Memory<T>也是Sliceable type,它是Span无法驻留到堆上的解决方案。一般Span<T>由底层开发人员用在数据同步处理和转换方面,而高层开发人员使用Memory<T>比较多,因为它可以用于一些高级的场景,比如:异步方法、类字段、lambda表达式、泛型参数等等。两者的完美运用就能够支持不复制地流动数据,这就是数据管道应用场景(System.IO.Pipelines)。

到目前为止,作者花了三篇博客终于把这两个旗舰类型讲完了,相信认真品读这三篇博客的同学,一定会受益匪浅。后面的系列将讲两者的高级应用场景,比如数据管道(Data Pipelines )、不连续缓冲区(Discontiguous Buffers)、缓冲池(Buffer Pooling)、以及为什么让Aspnet Core Web Server变得如此高性能等。
一图胜千言:

最新一期techempower web框架基准测试传送门

最后

如果有什么疑问和见解,欢迎评论区交流。
如果你觉得本篇文章对您有帮助的话,感谢您的【推荐】。
如果你对**.NET高性能编程感兴趣的话可以【关注我】**,我会定期的在博客分享我的学习心得。
欢迎转载,请在明显位置给出出处及链接

延伸阅读

https://en.wikipedia.org/wiki/Reference_counting

https://msdn.microsoft.com/en-us/magazine/mt814808

https://blogs.msdn.microsoft.com/oldnewthing/20040406-00/?p=39903

https://github.com/dotnet/corefxlab/blob/master/docs/specs/memory.md

https://blogs.msdn.microsoft.com/dotnet/2018/05/30/announcing-net-core-2-1

https://docs.microsoft.com/zh-cn/dotnet/api/system.memory-1?view=netcore-2.2

https://frameworkbenchmarks.readthedocs.io/en/latest/Project-Information/Framework-Tests

https://blogs.msdn.microsoft.com/dotnet/2018/07/09/system-io-pipelines-high-performance-io-in-net

https://www.codemag.com/Article/1807051/Introducing-.NET-Core-2.1-Flagship-Types-Span-T-and-Memory-T

https://docs.microsoft.com/en-us/previous-versions/windows/silverlight/dotnet-windows-silverlight/khk3k17t(v=vs.95)

https://blogs.msdn.microsoft.com/mazhou/2018/03/25/c-7-series-part-10-spant-and-universal-memory-management

<think>好的,我现在需要帮助用户理解如何正确使用`static std::shared_ptr<T> make`这个方法,特别是在他们提到的`t->template ref<T>()`的上下文中。首先,我应该回顾之前的对话,确保我了解用户已经掌握的内容。 用户之前询问了关于`t->template ref<T>()`的详细解释,现在他们想知道如何正确使用`static std::shared_ptr<T> make`。根据之前的回答,`make`函数是用来安全创建对象并返回`shared_ptr`的,用户可能需要了解如何实现和使用这个工厂方法。 接下来,我需要确定用户的使用场景。他们可能正在使用继承自`enable_shared_from_this`的类,并且需要正确管理对象的生命周期,避免在构造函数中调用`shared_from_this`导致的问题。`make`函数的作用是确保对象已经被`shared_ptr`管理,从而安全地调用`ref<T>()`方法。 我需要分步骤解释`make`函数的实现,包括使用`std::make_shared`创建对象,然后调用`ref<T>()`方法。同时,要强调为什么不能直接在构造函数中使用`shared_from_this`,以及工厂方法如何解决这个问题。 用户可能对模板参数和完美转发不太熟悉,所以需要简明地解释`Args&&... args`和`std::forward`的作用。此外,应该提供使用示例,说明如何调用`make`函数,并指出注意事项,比如确保正确继承和避免在构造期间调用`ref`。 另外,需要提醒用户潜在的风险,比如如果`T`没有正确继承`enable_shared_from_this`,或者没有使用工厂方法而直接构造对象,可能会导致未定义行为。最后,总结关键点,帮助用户记忆重要内容。 在组织回答时,要保持结构清晰,使用标题和代码块提高可读性,并确保语言通俗易懂,避免过于专业的术语,除非必要。同时,检查LaTeX格式是否正确,确保数学表达式和代码示例无误。</think>以下是对`static std::shared_ptr<T> make`方法的详细解析和使用指南: $$ \begin{aligned} &\text{make} \quad \rightarrow \quad \text{工厂方法} \\ &\text{static} \quad \rightarrow \quad \text{静态成员函数} \\ &\text{shared_ptr<T>} \quad \rightarrow \quad \text{返回智能指针} \end{aligned} $$ ### 一、核心作用 ```cpp template <class T, class... Args> static std::shared_ptr<T> make(Args&&... args) { // 关键两步: auto ptr = std::make_shared<T>(std::forward<Args>(args)...); // ① 构造对象 return ptr->template ref<T>(); // ② 安全获取指针 } ``` #### 1. 解决的核心问题 - **对象所有权管理**:确保对象在被`ref()`方法访问前,已经被`shared_ptr`正确管理 - **构造安全**:避免在构造函数中直接调用`shared_from_this()`(会导致未定义行为) #### 2. 典型应用场景 ```mermaid graph LR A[用户调用make] --> B[构造T对象] B --> C[立即包装为shared_ptr] C --> D[安全调用ref<T>] D --> E[返回增强型智能指针] ``` ### 二、关键实现步骤 #### 1. 完美转发参数 ```cpp Args&&... args // 通用引用接收任意参数 std::forward<Args>(args)... // 完美转发保持值类别 ``` #### 2. 构造对象 ```cpp std::make_shared<T>(...) // 优于new的3个原因: // ① 单次内存分配(控制块+对象) // ② 异常安全 // ③ 立即获得shared_ptr所有权 ``` #### 3. 调用ref方法 ```cpp ptr->template ref<T>() // 等效于: std::static_pointer_cast<T>(ptr->shared_from_this()) ``` ### 、使用示例 #### 1. 类定义要求 ```cpp class MyClass : public std::enable_shared_from_this<MyClass> { public: template<typename U> std::shared_ptr<U> ref() { return std::static_pointer_cast<U>(shared_from_this()); } }; ``` #### 2. 客户端调用 ```cpp auto obj = MyClass::make<MyClass>(构造参数); // 等价于: auto obj = std::static_pointer_cast<MyClass>( std::make_shared<MyClass>(构造参数)->shared_from_this() ); ``` ### 四、注意事项 | 关键点 | 正确示例 | 错误示例 | |-------------------------|-----------------------------|-----------------------------| | 调用时机 | 对象完全构造后 | 构造函数内调用 | | 所有权状态 | 已有shared_ptr持有 | 裸指针调用ref() | | 类型继承 | 正确继承enable_shared_from_this | 忘记继承基类 | | 模板参数 | make<具体类型> | make<错误类型> | ### 五、风险规避 1. **构造期间保护** ```cpp class SafeClass : public std::enable_shared_from_this<SafeClass> { protected: SafeClass() = default; // 防止外部直接实例化 friend class std::shared_ptr<SafeClass>; }; ``` 2. **SFINAE约束** ```cpp template<typename T, typename = std::enable_if_t<std::is_base_of_v< std::enable_shared_from_this<T>, T>>> static std::shared_ptr<T> make(Args&&... args) ``` ### 六、性能对比 ```cpp // 传统方式(2次内存分配) auto p = new MyClass(); auto sp = std::shared_ptr<MyClass>(p); // make方式(单次内存分配) auto sp = MyClass::make<MyClass>(); ``` 关键总结: 该工厂方法通过`std::make_shared`与`ref()`的配合,实现了: ① 类型安全的指针转换 ② 最优化的内存分配 ③ 安全的共享指针生命周期管理 ④ 强制的正确使用模式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值