Как вернуть массив в ArrayPool, когда он был арендован внутренней функцией?

У меня есть сценарий, когда мне нужно работать с большим массивом внутри некоторой внутренней функции (службы), но результат этой операции должен быть использован (сериализован в 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, поэтому он никогда не используется повторно.

Я рассмотрел несколько вариантов, но хочу знать, какая реализация лучше всего?

Вариант 1. Возврат родительской функцией

Мне не нравится этот вариант, потому что 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;
}

Вариант 2. Аренда и возврат с помощью родительской функции и передача в качестве ссылки.

Это лучше, но не будет работать, если 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
}

Вариант 3 – Аренда и возврат по разным функциям

Это будет работать, когда внутренняя функция определяет необходимое количество/длину.

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;
}

Есть ли другие варианты получше?

Попробуйте какой-нибудь многоразовый поток вместо байтовых массивов github.com/Microsoft/Microsoft.IO.RecyclableMemoryStream

Charlieface 31.03.2024 13:57

@Charlieface Я не использую байтовые массивы.

Maciej Pszczolinski 31.03.2024 16:59
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
122
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вместо любого из вышеперечисленных я бы использовал базовый шаблон удаления, чтобы вернуть некоторый 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 — я правильно понимаю?

Maciej Pszczolinski 01.04.2024 09:13

@MaciejPszczolinski - верно. Все это предполагает, что OkObjectResult.Value перечисляется только один раз. Если он указан дважды, например. (и я здесь просто размышляю) из-за того, что вы установили какое-то промежуточное программное обеспечение для ведения журналов, это предположение не работает, и вам нужно использовать другое решение, например второе решение.

dbc 01.04.2024 17:56

Другие вопросы по теме