مع هذه المقالة ، أواصل نشر سلسلة من المقالات ، ستكون نتائجه كتابًا عن عمل .NET CLR و .NET بشكل عام. للروابط - مرحبا بك في القط.
الذاكرة <T> و ReadOnlyMemory <T>
هناك اختلافان مرئيان بين Memory<T>
و Span<T>
. الأول هو أن نوع Memory<T>
لا يحتوي على قيد ref
في رأس النوع. بمعنى آخر ، نوع Memory<T>
لها الحق في أن تكون ليس فقط على المكدس ، إما أن تكون متغيرًا محليًا أو معلمة للطريقة أو قيمتها المرتجعة ، ولكن أيضًا في الكومة ، تشير من هناك إلى بعض البيانات في الذاكرة. ومع ذلك ، فإن هذا الاختلاف الصغير يحدث فرقًا كبيرًا في سلوك وقدرات Memory<T>
مقارنةً Span<T>
. على عكس Span<T>
، وهو وسيلة لاستخدام مخزن بيانات مؤقت لبعض الطرق ، Memory<T>
تصميم Memory<T>
لتخزين المعلومات حول المخزن المؤقت ، وليس للعمل معه.
ملاحظة
لم يتم تحديث الفصل المنشور على حبري ، وربما يكون قديمًا بالفعل. وبالتالي ، يرجى الرجوع إلى النص الأصلي للحصول على نص أحدث:

من هنا يأتي الاختلاف في API:
- لا تحتوي
Memory<T>
على طرق الوصول إلى البيانات التي تديرها. بدلاً من ذلك ، يحتوي على خاصية Span
وأسلوب Slice
، اللذين يعيدان حصان العمل - مثيل من نوع Span
. - بالإضافة إلى ذلك ، تحتوي
Memory<T>
على طريقة Pin()
المصممة للبرامج النصية عندما يجب تمرير المخزن المؤقت المخزن إلى رمز unsafe
. عندما يتم استدعاؤها للحالات التي تم فيها تخصيص الذاكرة في .NET ، سيتم تثبيت المخزن المؤقت ولن يتحرك عندما يتم تشغيل GC ، مما يعيد إلى المستخدم MemoryHandle
بنية MemoryHandle
، التي تغلف مفهوم عمر GCHandle
الذي تم إصلاح المخزن المؤقت في الذاكرة:
public unsafe struct MemoryHandle : IDisposable { private void* _pointer; private GCHandle _handle; private IPinnable _pinnable; /// <summary> /// MemoryHandle /// </summary> public MemoryHandle(void* pointer, GCHandle handle = default, IPinnable pinnable = default) { _pointer = pointer; _handle = handle; _pinnable = pinnable; } /// <summary> /// , , /// </summary> [CLSCompliant(false)] public void* Pointer => _pointer; /// <summary> /// _handle _pinnable, /// </summary> public void Dispose() { if (_handle.IsAllocated) { _handle.Free(); } if (_pinnable != null) { _pinnable.Unpin(); _pinnable = null; } _pointer = null; } }
ومع ذلك ، في البداية ، أقترح التعرف على مجموعة كاملة من الفئات. وكأول منهم ، ألق نظرة على بنية Memory<T>
(لا يتم عرض جميع أعضاء النوع ، ولكن يبدو أن هؤلاء هم الأكثر أهمية):
public readonly struct Memory<T> { private readonly object _object; private readonly int _index, _length; public Memory(T[] array) { ... } public Memory(T[] array, int start, int length) { ... } internal Memory(MemoryManager<T> manager, int length) { ... } internal Memory(MemoryManager<T> manager, int start, int length) { ... } public int Length => _length & RemoveFlagsBitMask; public bool IsEmpty => (_length & RemoveFlagsBitMask) == 0; public Memory<T> Slice(int start, int length); public void CopyTo(Memory<T> destination) => Span.CopyTo(destination.Span); public bool TryCopyTo(Memory<T> destination) => Span.TryCopyTo(destination.Span); }
بالإضافة إلى تحديد مجالات الهيكل ، قررت أن أشير أيضًا إلى أن هناك نوعين آخرين من internal
النوع internal
يعملان على أساس كيان آخر - MemoryManager
، والذي سيتم مناقشته أكثر قليلاً وهذا ليس شيئًا قد تحدثت عنه للتو الفكر: مدير ذاكرة بالمعنى الكلاسيكي. ومع ذلك ، مثل Span
، تحتوي Memory
أيضًا على مرجع إلى الكائن الذي سيتم التنقل فيه ، بالإضافة إلى إزاحة وحجم المخزن المؤقت الداخلي. من الجدير بالذكر أيضًا أنه يمكن إنشاء Memory
باستخدام المشغل new
فقط على أساس طرق الصفيف بالإضافة إلى التمديد - على أساس السلسلة ArraySegment
و ArraySegment
. على سبيل المثال لا يعني ضمناً إنشائه على أساس الذاكرة غير المُدارة. ومع ذلك ، كما نرى ، هناك بعض الطرق الداخلية لإنشاء هذه البنية استنادًا إلى MemoryManager
:
ملف MemoryManager.cs
public abstract class MemoryManager<T> : IMemoryOwner<T>, IPinnable { public abstract MemoryHandle Pin(int elementIndex = 0); public abstract void Unpin(); public virtual Memory<T> Memory => new Memory<T>(this, GetSpan().Length); public abstract Span<T> GetSpan(); protected Memory<T> CreateMemory(int length) => new Memory<T>(this, length); protected Memory<T> CreateMemory(int start, int length) => new Memory<T>(this, start, length); void IDisposable.Dispose() protected abstract void Dispose(bool disposing); }
سوف أسمح لنفسي أن أتجادل قليلاً مع المصطلحات التي تم تقديمها في أمر CLR ، مع تسمية النوع باسم MemoryManager. عندما رأيته ، قررت أولاً أن الأمر سيكون مثل إدارة الذاكرة ، ولكن يدويًا ، بخلاف LOH / SOH. لكنه شعر بخيبة أمل كبيرة لرؤية الواقع. ربما يجب عليك تسميته عن طريق القياس مع الواجهة: MemoryOwner.
الذي يلخص مفهوم مالك قطعة من الذاكرة. وبعبارة أخرى ، إذا كانت Span
وسيلة للعمل مع الذاكرة ، فإن Memory
هي وسيلة لتخزين المعلومات حول موقع معين ، فإن MemoryManager
هي وسيلة للتحكم في حياتها ، مالكها. على سبيل المثال ، يمكنك أن تأخذ نوع NativeMemoryManager<T>
، الذي ، على الرغم من أنه مكتوب للاختبارات ، لا يعكس بشكل سيء جوهر مفهوم "الملكية":
ملف NativeMemoryManager.cs
internal sealed class NativeMemoryManager : MemoryManager<byte> { private readonly int _length; private IntPtr _ptr; private int _retainedCount; private bool _disposed; public NativeMemoryManager(int length) { _length = length; _ptr = Marshal.AllocHGlobal(length); } public override void Pin() { ... } public override void Unpin() { lock (this) { if (_retainedCount > 0) { _retainedCount--; if (_retainedCount == 0) { if (_disposed) { Marshal.FreeHGlobal(_ptr); _ptr = IntPtr.Zero; } } } } } // }
بمعنى آخر ، يوفر الفصل إمكانية إجراء مكالمات متداخلة مع طريقة Pin()
، وبالتالي حساب الروابط الناتجة من العالم unsafe
.
كيان آخر يرتبط ارتباطًا وثيقًا Memory
هو MemoryPool
، والذي يوفر MemoryPool
لمثيلات MemoryManager
(وفي الواقع ، IMemoryOwner
):
ملف MemoryPool.cs
public abstract class MemoryPool<T> : IDisposable { public static MemoryPool<T> Shared => s_shared; public abstract IMemoryOwner<T> Rent(int minBufferSize = -1); public void Dispose() { ... } }
وهو مصمم لإصدار المخازن المؤقتة بالحجم المطلوب للاستخدام المؤقت. المثيلات المؤجرة التي تنفذ واجهة IMemoryOwner<T>
لها أسلوب Dispose()
الذي يعيد الصفيف المؤجر إلى تجمع الصفيف. وبشكل افتراضي ، يمكنك استخدام تجمع المخزن المؤقت المشترك ، الذي تم إنشاؤه على أساس ArrayMemoryPool
:
ملف ArrayMemoryPool.cs
internal sealed partial class ArrayMemoryPool<T> : MemoryPool<T> { private const int MaximumBufferSize = int.MaxValue; public sealed override int MaxBufferSize => MaximumBufferSize; public sealed override IMemoryOwner<T> Rent(int minimumBufferSize = -1) { if (minimumBufferSize == -1) minimumBufferSize = 1 + (4095 / Unsafe.SizeOf<T>()); else if (((uint)minimumBufferSize) > MaximumBufferSize) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBufferSize); return new ArrayMemoryPoolBuffer(minimumBufferSize); } protected sealed override void Dispose(bool disposing) { } }
وبناءً على ما رآه ، تلوح صورة العالم التالية:
- يجب استخدام نوع بيانات
Span
في معلمات الطريقة إذا كنت تقصد إما قراءة البيانات ( ReadOnlySpan
) أو الكتابة ( Span
). ولكن ليس مهمة تخزينها في مجال الصف لاستخدامها في المستقبل - إذا كنت بحاجة إلى تخزين ارتباط بمخزن البيانات المؤقت من حقل الفئة ، فيجب استخدام
Memory<T>
أو ReadOnlyMemory<T>
- اعتمادًا على الغرض MemoryManager<T>
هو مالك مخزن البيانات المؤقت (لا يمكنك استخدامه: إذا لزم الأمر). من الضروري ، على سبيل المثال ، عند الحاجة إلى حساب المكالمات Pin()
. أو عندما تحتاج إلى معرفة كيفية تحرير الذاكرة- إذا
Memory
بناء Memory
حول منطقة ذاكرة غير مُدارة ، Pin()
يقوم Pin()
بأي شيء. ومع ذلك ، فإن هذا يوحد العمل مع أنواع مختلفة من المخازن المؤقتة: في حالة التعليمات البرمجية المُدارة وفي حالة التعليمات البرمجية غير المُدارة ، ستكون واجهة التفاعل هي نفسها - كل نوع من الأنواع له منشئات عامة. هذا يعني أنه يمكنك استخدام كل من
Span
مباشرة والحصول على نسخة منه من Memory
. يمكنك إنشاء Memory
نفسها إما بشكل منفصل أو ترتيب نوع من نوع IMemoryOwner
يمتلك جزء الذاكرة الذي IMemoryOwner
إليه Memory
. قد تكون حالة خاصة من أي نوع استنادًا إلى MemoryManager
: بعض الملكية المحلية لقطعة من الذاكرة (على سبيل المثال ، مع حساب مرجعي من عالم unsafe
). إذا كنت بحاجة في نفس الوقت إلى سحب مثل هذه المخازن المؤقتة (توقع حركة مرور متكررة للمخازن المؤقتة ذات الحجم المتساوي تقريبًا) ، يمكنك استخدام نوع MemoryPool
. - إذا كان يعني ضمنيًا أنك بحاجة إلى العمل باستخدام رمز
unsafe
، أو تمرير مخزن مؤقت للبيانات هناك ، فيجب عليك استخدام نوع Memory
: فهو يحتوي على طريقة Pin
تعمل تلقائيًا على إصلاح المخزن المؤقت في كومة .NET إذا تم إنشاؤه هناك. - إذا كان لديك بعض حركة مرور المخزن المؤقت (على سبيل المثال ، يمكنك حل مشكلة تحليل نص البرنامج أو بعض DSL) ، فمن الجدير استخدام نوع
MemoryPool
، والذي يمكن تنظيمه بطريقة صحيحة جدًا ، وإخراج المخازن المؤقتة بالحجم المناسب من التجمع (على سبيل المثال ، أكبر قليلاً إذا لم يكن مناسبًا ولكن مع التقليم originalMemory.Slice(requiredSize)
حتى لا يتم تجزئة التجمع)
رابط للكتاب كله
