Шаблон підрахунку посилань для мов, керованих пам'яттю?


11

Java та .NET мають чудові сміттєзбірники, які керують пам’яттю для вас, та зручні схеми для швидкого звільнення зовнішніх об’єктів ( Closeable, IDisposable), але лише у тому випадку, якщо вони належать одному об’єкту. У деяких системах ресурс, можливо, потрібно буде споживати незалежно двома компонентами і вивільнятися лише тоді, коли обидва компоненти звільняють ресурс.

У сучасному C ++ ви вирішите цю проблему за допомогою a shared_ptr, що детерміновано звільнить ресурс, коли всі shared_ptrруйнуються.

Чи існують задокументовані, перевірені зразки управління та вивільнення дорогих ресурсів, які не мають єдиного власника в об'єктно-орієнтованих, недетермінованих системах збирання сміття?


1
Ви бачили автоматичний підрахунок посилань Кланг , який також використовується в Swift ?
jscs

1
@JoshCaswell Так, і це вирішило б проблему, але я працюю у зібраному сміттям просторі.
C. Росс

8
Довідковий підрахунок - це стратегія збору сміття.
Йорг W Міттаг

Відповіді:


15

Загалом, ви уникаєте цього, маючи одного власника - навіть некерованими мовами.

Але принцип однаковий для керованих мов. Замість того, щоб негайно закривати дорогий ресурс на Close()декременті лічильника (збільшується на Open()/ Connect()/ тощо), поки ви не натиснете 0, в якому моменту закриття фактично закривається. Це, швидше за все, буде виглядати і діяти, як Легка модель.


Про це я і думав, але чи існує це документально підтверджена закономірність? Flyweight, звичайно, схожий, але спеціально для пам'яті, як його зазвичай визначають.
К. Росс

@ C.Ross Це здається випадком, коли заохочуються фіналізатори. Ви можете використовувати клас обгортки навколо некерованого ресурсу, додавши до цього класу фіналізатор, щоб звільнити ресурс. Ви також можете його реалізувати IDisposable, продовжуйте рахувати, щоб якнайшвидше випустити ресурс тощо. Напевно, найкраще, багато разів, це мати усі три, але фіналізатор - це, мабуть, найважливіша частина, і IDisposableреалізація - це найменш критичний.
Panzercrisis

11
@Panzercrisis, за винятком того, що фіналізатори не гарантуються, і особливо не гарантується, що вони будуть працювати швидко .
Калет

@Caleth Я думав, що рахувати щось допоможе в частині оперативності. Наскільки вони взагалі не працюють, ти маєш на увазі, що CLR може просто не обійтись до закінчення програми, чи ти маєш на увазі, що вони можуть бути дискваліфіковані прямо?
Panzercrisis


14

У мові, зібраній зі сміттям (де GC не є детермінованою), неможливо надійно прив'язати очищення ресурсу, відмінного від пам'яті, до життя об’єкта: Неможливо вказати, коли об’єкт буде видалено. Кінець терміну експлуатації повністю на розсуд сміттєзбірника. ГК лише гарантує, що об’єкт буде жити, поки він буде доступний. Як тільки об'єкт стає недоступним, він може бути очищений в якийсь момент майбутнього, що може спричинити запуск фіналізаторів.

Поняття "володіння ресурсами" насправді не застосовується в мові GC. Система GC володіє всіма об'єктами.

Те, що ці мови пропонують із пробним ресурсом + Closeable (Java), використовуючи оператори + IDisposable (C #), або з операторами + менеджерами контексту (Python) - це спосіб управління потоком (! = Об'єкти) утримувати ресурс, який закривається, коли контрольний потік залишає область. У всіх цих випадках це аналогічно автоматично вставленому try { ... } finally { resource.close(); }. Термін експлуатації об’єкта, що представляє ресурс, не пов'язаний з терміном його експлуатації: об’єкт може продовжувати жити після закриття ресурсу, а об’єкт може стати недоступним, поки ресурс ще відкритий.

У випадку локальних змінних ці підходи еквівалентні RAII, але їх потрібно використовувати явно на сайті виклику (на відміну від деструкторів C ++, які працюватимуть за замовчуванням). Хороший IDE попередить, коли це пропущено.

Це не працює для об’єктів, на які посилаються місця, відмінні від локальних змінних. Тут не має значення, чи є одна чи кілька посилань. Можна перевести посилання на ресурси через посилання на об'єкти на право власності на ресурс через потік управління, створивши окремий потік, який містить цей ресурс, але потоки теж є ресурсами, які потрібно відкинути вручну.

У деяких випадках можливо делегувати право власності на ресурс на функцію виклику. Замість тимчасових об'єктів, що посилаються на ресурси, які вони повинні (але не можуть) надійно очистити, функція виклику містить набір ресурсів, які потрібно очистити. Це працює лише до тих пір, поки термін служби будь-якого з цих об'єктів не перевищує термін експлуатації, а отже, посилається на ресурс, який уже закритий. Цей компілятор не може виявити, якщо мова не має відстеження власності, як Rust (у цьому випадку вже є кращі рішення для цієї проблеми управління ресурсами).

Це залишається єдиним життєздатним рішенням: керування ресурсами вручну, можливо, шляхом самостійного підрахунку довідок. Це схильне до помилок, але не неможливо. Зокрема, думка про право власності є незвичною для мов GC, тому існуючий код може бути недостатньо явним щодо гарантій власності.


3

Багато хорошої інформації з інших відповідей.

Однак, щоб бути явним, шаблон, який ви можете шукати, полягає в тому, що ви використовуєте невеликі об'єкти, що належать окремо, для конструкції потоку управління, подібної RAII, usingі IDisposeв поєднанні з (більшим, можливо, посилальним) об'єктом, який містить деякий (працює система) ресурси.

Отже, є невеликі об'єкти без власного доступу до власника, які (через менший об'єкт IDisposeі usingконструкцію керуючого потоку) можуть у свою чергу інформувати про більший спільний об'єкт (можливо, користувацькі Acquireта Releaseметоди).

( Показані нижче методи Acquireта Releaseметоди також доступні і поза використовуваною конструкцією, але без безпеки tryнеявного в using.)


Приклад в C #

void Test ( MyRefCountedClass myObj )
{
    using ( var usingRef = myObj.Acquire () )
    {
        var item = usingRef.Item;
        item.SomeMethod ();

        // the `using` automatically invokes Dispose() on usingRef
        //  which in turn invokes Release() on `myObj.
    }
}

interface IReferencable<T> where T: IReferencable<T> {
    Reference<T> Acquire ();
    void Release();
}

struct Reference<T>: IDisposable where T: IReferencable<T>
{
    public readonly T Item;
    public Reference(T item) { Item = item; _released = false; }
    public void Dispose() { if (! _released ) { _released = true; Item.Release(); } }
    private bool _released;
}

class MyRefCountedClass : IReferencable<MyRefCountedClass>
{
    private int _refCount = 0;

    public Reference<MyRefCountedClass> Acquire ()
    {
        _refCount++;
        return new Reference<MyRefCountedClass>(this);
    }

    public void Release ()
    {
        if (--_refCount <= 0)
            Dispose();
    }

    // NOTE that MyRefCountedClass does not have to implement IDisposable, but it can...
    // as shown here it doesn't implement the interface
    private void Dispose ()  
    {
        if ( _refCount > 0 )
            throw new Exception ("Dispose attempted on item in use.");
        // release other resources...
    }

    public int SomeMethod()
    {
        return 0;
    }
}

Якщо це повинно бути C # (як це виглядає), то Ваша реалізація Reference <T> є невірною. У договорі IDisposable.Disposeзазначено, що виклик Disposeдекількох разів на один і той же об'єкт повинен бути необов'язковим. Якби я реалізував таку схему, я також став би Releaseприватним, щоб уникнути зайвих помилок і використовувати делегування замість успадкування (видаліть інтерфейс, надайте простий SharedDisposableклас, який можна використовувати з довільними одноразовими товарами), але це більше питання смаку.
Во

@Voo, добре, хороший пункт, THX!
Ерік Ейдт

1

Переважна більшість об'єктів в системі, як правило, відповідає одному з трьох шаблонів:

  1. Об'єкти, стан яких ніколи не зміниться, і на які посилання вважаються виключно засобом інкапсуляції держави. Суб'єкти, що мають посилання, не знають і не цікавляться, чи мають будь-які інші суб'єкти посилання на один і той же об'єкт.

  2. Об'єкти, які перебувають під винятковою владою одного суб'єкта господарювання, який є єдиним власником усього стану в ньому, і використовує об'єкт виключно як засіб інкапсуляції (можливо змінного) стану в ньому.

  3. Об'єкти, які належать одній особі, але які інші суб'єкти можуть використовувати обмежено. Власник об'єкта може використовувати його не тільки як засіб інкапсуляції стану, але і інкапсуляцію відносин з іншими суб'єктами, які його ділять.

Відстеження збору сміття працює краще, ніж підрахунок посилань №1, оскільки код, який використовує такі об'єкти, не повинен робити нічого особливого, коли це робиться з останньою рештою, що залишилася. Підрахунок посилань не потрібен №2, оскільки об’єкти матимуть саме одного власника, і він буде знати, коли йому більше не потрібен об’єкт. Сценарій №3 може скласти певні труднощі, якщо власник об'єкта вбиває його, тоді як інші об'єкти все ще зберігають посилання; навіть там відстеження GC може бути кращим, ніж підрахунок посилань, щоб гарантувати, що посилання на мертві об'єкти залишаються надійно ідентифікованими як посилання на мертві об'єкти до тих пір, поки існують такі посилання.

Є кілька ситуацій, коли може знадобитися об'єкт без власного власника придбавати та зберігати зовнішні ресурси до тих пір, поки комусь потрібні його послуги, і він повинен звільняти їх, коли його послуги більше не потрібні. Наприклад, об’єкт, який інкапсулює вміст файлу лише для читання, може бути спільним та використаний багатьма об'єктами одночасно, без жодного з них не потрібно знати або дбати про існування один одного. Однак такі обставини рідкісні. Більшість об’єктів або матимуть одного чіткого власника, або ж не мають власника. Багаторазове право власності можливо, але рідко корисне.


0

Спільна власність рідко викликає сенс

Ця відповідь може бути дещо дотичною, але я мушу запитати, скільки випадків має сенс з точки зору користувача, щоб поділити право власності ? Принаймні, в доменах, в яких я працював, їх практично не було, тому що в іншому випадку це означатиме, що користувачеві не потрібно просто одноразово видаляти щось з одного місця, а явно видаляти його від усіх відповідних власників, перш ніж ресурс насправді буде видалено з системи.

Це часто інженерна ідея нижчого рівня, щоб запобігти руйнуванню ресурсів, поки щось інше все ще доступ до неї, як інша нитка. Часто, коли користувач просить закрити / видалити / видалити щось із програмного забезпечення, його слід видалити якомога швидше (коли це безпечно видалити), і він, безумовно, не повинен затримуватися і спричиняти витік ресурсів настільки довго, програма працює.

Наприклад, ігровий актив у відеоігри може посилатися на матеріал із бібліотеки матеріалів. Ми, звичайно, не хочемо, скажімо, збиття звисаючого вказівника, якщо матеріал видаляється з бібліотеки матеріалів одним потоком, а інший потік все ще отримує доступ до матеріалу, на який посилається ігровий актив. Але це не означає, що для ігрових активів немає сенсу розділяти право власності на матеріали, на які вони посилаються, з бібліотекою матеріалів. Ми не хочемо примушувати користувача явно видаляти матеріал як з ресурсу, так і з бібліотеки матеріалів. Ми просто хочемо переконатися, що матеріали не видаляються з бібліотеки матеріалів, єдиного розумного власника матеріалів, поки інші теми не закінчать доступ до матеріалу.

Витоки ресурсів

Однак я працював з колишньою командою, яка охоплювала GC для всіх компонентів програмного забезпечення. І хоча це справді допомогло переконатися, що ми ніколи не знищували ресурси, а інші потоки все ще отримували доступ до них, ми натомість отримували свою частку витоків ресурсів .

І це були не тривіальні витоки ресурсів, які засмучують лише розробників, як кілобайт пам'яті, що просочився після багатогодинного сеансу. Це були епічні витоки, часто гігабайти пам'яті протягом активного сеансу, що призводило до звітів про помилки. Тому що зараз, коли на власність ресурсу посилається (і, отже, поділяється право власності) серед, скажімо, 8 різних частин системи, тоді потрібно лише одну, щоб не видалити ресурс у відповідь на те, що користувач просить його видалити бути просоченим і, можливо, на невизначений термін.

Тому я ніколи не був великим шанувальником GC або підрахунку довідок, застосовуваних в будь-яких широких масштабах через те, наскільки легко вони створили пропускне програмне забезпечення. Те, що раніше було б звисаючим крахом вказівника, який легко виявити, перетворюється на дуже важко виявити витік ресурсу, який легко може летіти під радари тестування.

Слабкі посилання можуть пом'якшити це питання, якщо мова / бібліотека надає це, але мені було важко отримати команду розробників змішаних наборів вмінь, щоб мати можливість постійно використовувати слабкі посилання, коли це доречно. І ця складність стосувалася не лише внутрішньої команди, але і кожного розробника плагінів для нашого програмного забезпечення. Вони також могли легко призвести до витоку системи через просто збереження постійної посилання на об'єкт таким чином, що важко було простежити за плагіном як винуватцем, тому ми також отримали нашу левову частку звітів про помилки в результаті наших програмних ресурсів витік просто тому, що плагін, вихідний код якого був поза нашим контролем, не зміг випустити посилання на ці дорогі ресурси.

Рішення: відкладено, періодичне видалення

Тому згодом моє рішення, в якому я звернувся до своїх особистих проектів, які дали мені найкраще, що я знайшов, з обох світів, полягало в тому, щоб усунути концепцію, яка, referencing=ownershipале все ще відклала руйнування ресурсів.

Як наслідок, тепер, коли користувач робить щось, що викликає необхідність видалення ресурсу, API виражається в простому видаленні ресурсу:

ecs->remove(component);

... який дуже просто відтворює логіку користувача. Однак ресурс (компонент) не може бути видалений відразу, якщо у фазі їх обробки є інші системні потоки, де вони можуть одночасно отримувати доступ до того ж компонента.

Тож ці нитки обробки потім дають час туди-сюди, що дозволяє потоці, що нагадує сміттєзбірник, прокидатися і " зупиняти світ " і знищувати всі ресурси, які вимагали видалити, блокуючи потоки від обробки цих компонентів, поки вона не буде закінчена . Я налаштував це настільки, що кількість робіт, які потрібно виконати тут, як правило, мінімальна і не помітно скорочується на частоту кадрів.

Зараз я не можу сказати, що це якийсь перевірений і добре задокументований метод, але це те, що я використовую вже кілька років, без головних болів і без витоків ресурсів. Я рекомендую досліджувати такі підходи, коли можливо, щоб ваша архітектура підходила до такої моделі паралельної валюти, оскільки це набагато менш важко, ніж GC або перерахунок переліку, і не ризикує ці види витоків ресурсів летіти під радари тестування.

Єдине місце, де я вважаю перерахунок посилань або GC корисним - це стійкі структури даних. У такому випадку це територія структури даних, далека від проблем, пов'язаних з користувачем, і там фактично має сенс для кожної непорушної копії потенційно ділитися правом власності на ті самі немодифіковані дані.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.