// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; #nullable enable namespace System.Buffers; /// /// Used to allocate and distribute re-usable blocks of memory. /// internal sealed class PinnedBlockMemoryPool : MemoryPool { /// /// The size of a block. 4096 is chosen because most operating systems use 4k pages. /// private const int _blockSize = 4096; /// /// Max allocation block size for pooled blocks, /// larger values can be leased but they will be disposed after use rather than returned to the pool. /// public override int MaxBufferSize { get; } = _blockSize; /// /// The size of a block. 4096 is chosen because most operating systems use 4k pages. /// public static int BlockSize => _blockSize; /// /// Thread-safe collection of blocks which are currently in the pool. A slab will pre-allocate all of the block tracking objects /// and add them to this collection. When memory is requested it is taken from here first, and when it is returned it is re-added. /// private readonly ConcurrentQueue _blocks = new ConcurrentQueue(); /// /// This is part of implementing the IDisposable pattern. /// private bool _isDisposed; // To detect redundant calls private readonly object _disposeSync = new object(); /// /// This default value passed in to Rent to use the default value for the pool. /// private const int AnySize = -1; public override IMemoryOwner Rent(int size = AnySize) { if (size > _blockSize) { MemoryPoolThrowHelper.ThrowArgumentOutOfRangeException_BufferRequestTooLarge(_blockSize); } if (_isDisposed) { MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPool); } if (_blocks.TryDequeue(out var block)) { // block successfully taken from the stack - return it return block; } return new MemoryPoolBlock(this, BlockSize); } /// /// Called to return a block to the pool. Once Return has been called the memory no longer belongs to the caller, and /// Very Bad Things will happen if the memory is read of modified subsequently. If a caller fails to call Return and the /// block tracking object is garbage collected, the block tracking object's finalizer will automatically re-create and return /// a new tracking object into the pool. This will only happen if there is a bug in the server, however it is necessary to avoid /// leaving "dead zones" in the slab due to lost block tracking objects. /// /// The block to return. It must have been acquired by calling Lease on the same memory pool instance. internal void Return(MemoryPoolBlock block) { #if BLOCK_LEASE_TRACKING Debug.Assert(block.Pool == this, "Returned block was not leased from this pool"); Debug.Assert(block.IsLeased, $"Block being returned to pool twice: {block.Leaser}{Environment.NewLine}"); block.IsLeased = false; #endif if (!_isDisposed) { _blocks.Enqueue(block); } } protected override void Dispose(bool disposing) { if (_isDisposed) { return; } lock (_disposeSync) { _isDisposed = true; if (disposing) { // Discard blocks in pool while (_blocks.TryDequeue(out _)) { } } } } }