У меня есть сценарий, когда мне нужно работать с большим массивом внутри некоторой внутренней функции (службы), но результат этой операции должен быть использован (сериализован в JSON и возвращен через HTTP) родительской функцией:
public IActionResult ParentFunction()
{
var returnedArray = InnerFunction(1000);
return Ok(returnedArray.Take(1000));
}
public int[] InnerFunction(int count)
{
var rentedArray = _defaultArrayPool.Rent(count);
// make operation on rentedArray
return rentedArray;
}
Очевидно, что в приведенном выше коде массив не возвращается в _defaultArrayPool, поэтому он никогда не используется повторно.
Я рассмотрел несколько вариантов, но хочу знать, какая реализация лучше всего?
Мне не нравится этот вариант, потому что Rent и Return вызываются в разных частях кода.
public IActionResult ParentFunction()
{
int[] returnedArray = null;
try
{
returnedArray = InnerFunction(1000);
return Ok(returnedArray.Take(1000));
}
finally
{
if (returnedArray != null)
{
_defaultArrayPool.Return(returnedArray);
}
}
}
public int[] InnerFunction(int count)
{
var rentedArray = _defaultArrayPool.Rent(count);
// make operation on rentedArray
return rentedArray;
}
Это лучше, но не будет работать, если ParentFunction заранее не знает длину/счет.
public IActionResult ParentFunction()
{
var rentedArray = _defaultArrayPool.Rent(1000); // will not work if 'Count' is unknown here, and is to be determined by InnerFunction
try
{
InnerFunction(rentedArray, 1000);
return Ok(rentedArray.Take(1000));
}
finally
{
if (rentedArray != null)
{
_defaultArrayPool.Return(rentedArray);
}
}
}
public void InnerFunction(int[] arr, int count)
{
// make operation on arr
}
Это будет работать, когда внутренняя функция определяет необходимое количество/длину.
public IActionResult ParentFunction()
{
int[] rentedArray = null;
try
{
var count = InnerFunction(out rentedArray);
return Ok(rentedArray.Take(count));
}
finally
{
if (rentedArray != null)
{
_defaultArrayPool.Return(rentedArray);
}
}
}
public int InnerFunction(out int[] arr)
{
int count = 1000; // determin lenght of the array
arr = _defaultArrayPool.Rent(count);
// make operation on arr
return count;
}
Есть ли другие варианты получше?
@Charlieface Я не использую байтовые массивы.





Вместо любого из вышеперечисленных я бы использовал базовый шаблон удаления, чтобы вернуть некоторый IDisposable, который оборачивает арендованный массив и возвращает его при удалении.
Сначала определите следующую одноразовую обертку:
public sealed class RentedArrayWrapper<T> : IList<T>, IDisposable
{
public T [] array;
readonly ArrayPool<T>? pool;
readonly int count;
public static RentedArrayWrapper<T> Create(ArrayPool<T> pool, int count) => new RentedArrayWrapper<T>(pool.Rent(count), pool, count);
RentedArrayWrapper(T [] array, ArrayPool<T>? pool,int count)
{
if (count < 0 || count > array.Length)
throw new ArgumentException("count < 0 || count > array.Length");
this.array = array ?? throw new ArgumentNullException(nameof(array));
this.pool = pool;
this.count = count;
}
public T [] Array => array ?? throw new ObjectDisposedException(GetType().Name);
public Memory<T> Memory => Array.AsMemory().Slice(0, count);
public T this[int index]
{
get
{
if (index < 0 || index >= count)
throw new ArgumentOutOfRangeException();
return Array[index];
}
set
{
if (index < 0 || index >= count)
throw new ArgumentOutOfRangeException();
Array[index] = value;
}
}
public IEnumerable<T> EnumerateAndDispose()
{
IEnumerable<T> EnumerateAndDisposeInner()
{
try
{
foreach (var item in this)
yield return item;
}
finally
{
Dispose();
}
}
CheckDisposed();
return EnumerateAndDisposeInner();
}
public IEnumerator<T> GetEnumerator()
{
IEnumerator<T> GetEnumeratorInner()
{
CheckDisposed();
for (int i = 0; i < count; i++)
yield return this[i];
}
CheckDisposed();
return GetEnumeratorInner();
}
public int IndexOf(T item) => System.Array.IndexOf<T>(Array, item, 0, count);
public bool Contains(T item) => IndexOf(item) >= 0;
public void CopyTo(T[] array, int arrayIndex) => Memory.CopyTo(array.AsMemory().Slice(arrayIndex));
public int Count => count;
void IList<T>.Insert(int index, T item) => throw new NotImplementedException();
void IList<T>.RemoveAt(int index) => throw new NotImplementedException();
void ICollection<T>.Add(T item) => throw new NotImplementedException();
void ICollection<T>.Clear() => throw new NotImplementedException();
bool ICollection<T>.Remove(T item) => throw new NotImplementedException();
bool ICollection<T>.IsReadOnly => true; // Indicates items cannot be added or removed
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
void CheckDisposed()
{
if (this.array == null)
throw new ObjectDisposedException(GetType().Name);
}
void Dispose(bool disposing)
{
if (disposing)
if (Interlocked.Exchange(ref this.array, null!) is {} array)
pool?.Return(array);
}
}
public static partial class ArrayPoolExtensions
{
public static RentedArrayWrapper<T> RentWrapper<T>(this ArrayPool<T> pool, int count) => RentedArrayWrapper<T>.Create(pool, count);
}
И теперь вы можете написать свои родительские и внутренние функции следующим образом:
public IActionResult ParentFunction()
{
using var wrapper = InnerFunction(1000);
// Take() uses deferred execution so we must materialize the rented array into a final non-disposable result so that
// ObObjectResult.ExecuteResultAsync(ActionContext context) does not attempt to serialize the rented array after it has been returned.
return Ok(wrapper.Take(1000).ToArray());
}
public RentedArrayWrapper<int> InnerFunction(int count)
{
var wrapper = _defaultArrayPool.RentWrapper(count);
// make operation on wrapper.Array
return wrapper;
}
Макет скрипки №1 здесь.
При этом существует фундаментальная проблема всех ваших реализаций: вы возвращаете арендованный массив обратно в пул массивов до того, как ваш OkObjectResult фактически будет выполнен и значение сериализовано в поток ответов. Таким образом, то, что вы возвращаете, вполне может быть случайным, если та же самая память пула массива впоследствии будет арендована в другом месте в промежуточный период.
Какие у вас есть варианты обойти это? На ум приходит пара вариантов.
Во-первых, вы могли бы рассмотреть возможность возврата некоторой перечислимой оболочки, которая удаляет RentedArrayWrapper после одной итерации, например:
public IActionResult ParentFunction()
{
var wrapper = InnerFunction(1000);
return Ok(wrapper.EnumerateAndDispose());
}
public RentedArrayWrapper<int> InnerFunction(int count)
{
var wrapper = _defaultArrayPool.RentWrapper(count);
// make operation on wrapper.Array
return wrapper;
}
Хотя это работает, мне это кажется схематичным, потому что мое общее мнение таково, что счетчики не должны иметь побочных эффектов. Макет скрипки №2 здесь.
Во-вторых, вы можете рассмотреть возможность создания подкласса OkObjectResult, который является типом, возвращаемым ControllerBase.Ok(object), и заставить его распоряжаться своим значением после выполнения, например:
public class OkDisposableResult : OkObjectResult
{
public OkDisposableResult(IDisposable disposable) : base(disposable) { }
public override async Task ExecuteResultAsync(ActionContext context)
{
try
{
await base.ExecuteResultAsync(context);
}
finally
{
if (Value is IDisposable disposable)
disposable.Dispose();
}
}
public override void ExecuteResult(ActionContext context)
{
// I'm not sure this is ever actually called
try
{
base.ExecuteResult(context);
}
finally
{
if (Value is IDisposable disposable)
disposable.Dispose();
}
}
}
А затем возвращаем оболочку арендованного массива следующим образом:
public IActionResult ParentFunction()
{
var wrapper = InnerFunction(1000);
return new OkDisposableResult(wrapper);
}
public RentedArrayWrapper<int> InnerFunction(int count)
{
var wrapper = _defaultArrayPool.RentWrapper(count);
// make operation on wrapper.Array
return wrapper;
}
Макет скрипки №3 здесь.
Очень умно. Массив по сути возвращается в пул. Можно ли быть уверенным, что finally в EnumerateAndDispose() выполняется ПОСЛЕ того, как данные были фактически записаны в поток ответов? Я думаю «ДА», и это гарантируется OkDisposableResult — я правильно понимаю?
@MaciejPszczolinski - верно. Все это предполагает, что OkObjectResult.Value перечисляется только один раз. Если он указан дважды, например. (и я здесь просто размышляю) из-за того, что вы установили какое-то промежуточное программное обеспечение для ведения журналов, это предположение не работает, и вам нужно использовать другое решение, например второе решение.
Попробуйте какой-нибудь многоразовый поток вместо байтовых массивов github.com/Microsoft/Microsoft.IO.RecyclableMemoryStream