Що саме є рецензуючою функцією?


198

Більшість з тих часів , визначення reentrance цитата з Вікіпедії :

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

  1. Не повинно містити статичних (або глобальних) непостійних даних.
  2. Не повинен повертати адресу до статичних (або глобальних) непостійних даних.
  3. Повинно працювати лише над даними, які йому надає абонент.
  4. Не повинно покладатися на блокування одиночних ресурсів.
  5. Не повинен змінювати власний код (якщо не виконувати у своєму унікальному сховищі потоків)
  6. Не повинно закликати комп’ютерні програми, що не рецидивуються, або підпрограми.

Як безпечно визначено?

Якщо програма може бути безпечно виконана одночасно , чи це завжди означає, що вона є ретентована?

Що саме є загальним рядком між згаданими шістьма пунктами, про які я повинен пам’ятати, перевіряючи код на наявність ретеннтних можливостей?

Також,

  1. Чи всі рекурсивні функції відновлюються?
  2. Чи всі функції, захищені від потоку, відновлюються?
  3. Чи є всі рекурсивні та безпечні для потоку функції?

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


6
Власне, я не погоджуюся з №2 у першому списку. Ви можете повернути адресу на все, що завгодно, з функції повторного вступу - обмеження стосується того, що ви робите з цією адресою в коді виклику.

2
@Neil Але оскільки автор функції рецензії не може контролювати те, що абонент, безумовно, точно не повинен повертати адресу статичним (або глобальним) непостійним даним, щоб він був справді ретентом?
Robben_Ford_Fan_boy

2
@drelihan Це не несе відповідальність письменника про будь-яку функцію (ретентована чи ні) контролювати те, що робить абонент із поверненим значенням. Вони, звичайно, повинні сказати, що абонент МОЖЕ зробити з цим, але якщо абонент вирішить зробити щось інше - неприємна удача для абонента.

"безпечно для потоків" є безглуздим, якщо ви також не вкажете, що вони роблять, і який очікуваний ефект від їх виконання. Але, можливо, це має бути окремим питанням.

Я сміливо маю на увазі, що поведінка є чітко визначеною та детермінованою незалежно від планування.
AturSams

Відповіді:


191

1. Як безпечно визначено?

Семантично. У цьому випадку це не важко визначений термін. Це просто означає "Ви можете це зробити без ризику".

2. Якщо програму можна безпечно виконувати одночасно, чи завжди це означає, що вона є ретентована?

Немає.

Наприклад, давайте функцію C ++, яка приймає як параметр і блокування, і зворотний виклик:

#include <mutex>

typedef void (*callback)();
std::mutex m;

void foo(callback f)
{
    m.lock();
    // use the resource protected by the mutex

    if (f) {
        f();
    }

    // use the resource protected by the mutex
    m.unlock();
}

Ще одна функція могла потребувати блокування того ж файлу:

void bar()
{
    foo(nullptr);
}

На перший погляд, все здається нормальним… Але зачекайте:

int main()
{
    foo(bar);
    return 0;
}

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

  1. mainподзвонить foo.
  2. foo придбає замок.
  3. fooподзвонить bar, який зателефонує foo.
  4. 2-й foo спробує придбати замок, вийде з ладу і чекати його звільнення.
  5. Тупик.
  6. На жаль ...

Гаразд, я обдурив, використовуючи річ зворотного дзвінка. Але легко уявити складніші фрагменти коду, що мають подібний ефект.

3. Що саме є загальним рядком між згаданими шістьма пунктами, про які я повинен пам’ятати, перевіряючи код на наявність можливостей рецензії?

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

( Гаразд, 99% нашого коду повинно пахнути, то ... Дивіться останній розділ, щоб вирішити це… )

Отже, вивчаючи свій код, один із цих моментів повинен вас насторожити:

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

Зауважте, що невідповідність є вірусною: Функція, яка могла викликати можливу функцію, яка не є ререрантом, не може вважатися ретентованною.

Зауважте, що методи C ++ пахнуть, оскільки їм доступнийthis , тому вам слід вивчити код, щоб переконатися, що вони не мають смішної взаємодії.

4.1. Чи всі рекурсивні функції відновлюються?

Немає.

У багатопотокових випадках рекурсивна функція, що отримує доступ до спільного ресурсу, може бути викликана декількома потоками в один і той же момент, що призводить до поганих / пошкоджених даних.

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

4.2. Чи всі функції, захищені від потоку, відновлюються?

У наведеному вище прикладі я показав, як функція безпечної передачі потоків очевидно не є ретранслятором. Гаразд, я обдурив через параметр зворотного дзвінка. Але тоді існує декілька способів тупикової нитки шляхом її отримання вдвічі нерекурсивного блокування.

4.3. Чи є всі рекурсивні та безпечні для потоку функції?

Я б сказав "так", якщо під "рекурсивним" ви маєте на увазі "рекурсивно-безпечний".

Якщо ви можете гарантувати, що функцію можна викликати одночасно декількома потоками і може без проблем викликати себе безпосередньо чи опосередковано, тоді вона є ретентованной.

Проблема полягає в оцінці цієї гарантії… ^ _ ^

5. Чи є абсолютними такі терміни, як відступ і безпека ниток, тобто чи мають вони конкретні визначення?

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

6. Приклад

Скажімо, у вас є об’єкт, з одним методом, який повинен використовувати ресурс:

struct MyStruct
{
    P * p;

    void foo()
    {
        if (this->p == nullptr)
        {
            this->p = new P();
        }

        // lots of code, some using this->p

        if (this->p != nullptr)
        {
            delete this->p;
            this->p = nullptr;
        }
    }
};

Перша проблема полягає в тому, що якщо якимось чином ця функція викликається рекурсивно (тобто ця функція викликає себе безпосередньо чи опосередковано), код, ймовірно, вийде з ладу, оскільки this->p буде видалений наприкінці останнього дзвінка і все ще, ймовірно, буде використовуватися до кінця першого дзвінка.

Таким чином, цей код не є рекурсивно-безпечним .

Ми можемо використовувати контрольний лічильник, щоб виправити це:

struct MyStruct
{
    size_t c;
    P * p;

    void foo()
    {
        if (c == 0)
        {
            this->p = new P();
        }

        ++c;
        // lots of code, some using this->p
        --c;

        if (c == 0)
        {
            delete this->p;
            this->p = nullptr;
        }
    }
};

Таким чином, код стає рекурсивно-безпечним ... Але він все ще не є ретрансляційним через багатопотокові проблеми: Ми повинні бути впевнені, що зміни cта виконання pбудуть виконані атомно, використовуючи рекурсивний мютекс (не всі мутекси рекурсивні):

#include <mutex>

struct MyStruct
{
    std::recursive_mutex m;
    size_t c;
    P * p;

    void foo()
    {
        m.lock();

        if (c == 0)
        {
            this->p = new P();
        }

        ++c;
        m.unlock();
        // lots of code, some using this->p
        m.lock();
        --c;

        if (c == 0)
        {
            delete this->p;
            this->p = nullptr;
        }

        m.unlock();
    }
};

І звичайно, це все передбачає, що lots of codeє самим ретенцією, включаючи використання p.

А наведений вище код навіть не є безпечним для винятку віддаленим , але це вже інша історія… ^ _ ^

7. Ей 99% нашого коду не є ретентом!

Це цілком справедливо для коду спагетті. Але якщо правильно розділити код, ви уникнете проблем із перезаписом.

7.1. Переконайтесь, що всі функції не мають стану

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

7.2. Переконайтеся, що ваш об'єкт "рекурсивно-безпечний"

Метод об'єкта має доступ до this, тому він поділяє стан з усіма методами одного і того ж примірника об'єкта.

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

7.3. Переконайтесь, що всі ваші об’єкти правильно інкапсульовані

Ніхто інший не повинен мати доступ до своїх внутрішніх даних:

    // bad
    int & MyObject::getCounter()
    {
        return this->counter;
    }

    // good
    int MyObject::getCounter()
    {
        return this->counter;
    }

    // good, too
    void MyObject::getCounter(int & p_counter)
    {
        p_counter = this->counter;
    }

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

7.4. Переконайтеся, що користувач знає, що ваш об’єкт не є безпечним для потоків

Таким чином, користувач несе відповідальність за використання мутексів для використання об'єкта, спільного між потоками.

Об'єкти з STL розроблені таким чином, щоб вони не були безпечними для потоків (через проблеми з продуктивністю), і, таким чином, якщо користувач хоче поділитися std::stringміж двома потоками, користувач повинен захистити свій доступ з одночасними примітивами;

7.5. Переконайтесь, що ваш захищений від потоку код є рекурсивно-безпечним

Це означає використовувати рекурсивні мютекси, якщо ви вважаєте, що один і той же потік можна використовувати один і той же ресурс двічі.


1
Щоб трохи посперечатися, я фактично думаю, що в цьому випадку визначено "безпеку" - це означає, що функція буде діяти лише на наданих змінних - тобто це скорочення для цитати визначення нижче неї. І справа в тому, що це може не означати інших ідей безпеки.
Joe Soul-donosi

Ви пропустили передачу в мютекс у першому прикладі?
детлей

@paercebal: ваш приклад неправильний. Насправді вам не потрібно турбуватися із зворотним дзвоном, проста рекурсія матиме таку ж проблему, якщо є така, проте єдиною проблемою є те, що ви забули сказати, де саме розподілено замок.
Іттріл

3
@Yttrill: Я припускаю, що ви говорите про перший приклад. Я використовував "зворотний дзвінок", оскільки, по суті, зворотний виклик пахне. Звичайно, рекурсивна функція мала б ту саму проблему, але, як правило, можна легко проаналізувати функцію та її рекурсивну природу, таким чином, виявити, чи є вона реєструючою чи це нормально для рекурсивності. З іншого боку, зворотний виклик означає, що автор функції, що викликає зворотний виклик, не має жодної інформації про те, що робить зворотний виклик, тому цьому автору може бути важко переконатися, що його функція відновлюється. Ось цю складність я хотів показати.
paercebal

1
@Gab 是 好人: Я виправив перший приклад. Дякую! Обробник сигналу може зіткнутися зі своїми проблемами, відмінними від повторної безпеки, як зазвичай, коли сигнал піднімається, ви не можете реально нічого зробити, крім зміни конкретно оголошеної глобальної змінної.
paercebal

21

"Безпечно" визначається саме так, як диктує здоровий глузд - це означає "робити свою справу правильно, не втручаючись в інші речі". Шість пунктів, які ви цитуєте, досить чітко висловлюють вимоги для досягнення цього.

Відповіді на ваші 3 запитання - 3 × "ні".


Чи всі рекурсивні функції відновлюються?

НЕМАЄ!

Два одночасні виклики рекурсивної функції можуть легко перекрутити один одного, наприклад, якщо вони отримують доступ до одних і тих же глобальних / статичних даних.


Чи всі функції, захищені від потоку, відновлюються?

НЕМАЄ!

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


Чи є всі рекурсивні та безпечні для потоку функції?

НЕМАЄ!

Дивись вище.


10

Загальна нитка:

Чи правильно визначена поведінка, якщо виклик рутини, коли вона перервана?

Якщо у вас є така функція:

int add( int a , int b ) {
  return a + b;
}

Тоді це не залежить від будь-якого зовнішнього стану. Поведінка чітко визначена.

Якщо у вас є така функція:

int add_to_global( int a ) {
  return gValue += a;
}

Результат недостатньо визначений у кількох потоках. Інформація може бути втрачена, якщо термін був просто неправильним.

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


7

Тепер я повинен детальніше розглянути свій попередній коментар. @paercebal відповідь невірна. У прикладі коду ніхто не помітив, що mutex, який, як і повинен був бути параметром, насправді не переданий?

Я заперечую висновок, я стверджую: щоб функція була безпечною за наявності одночасності, вона повинна бути повторною. Тому одночасне безпечне (як правило, письмове безпечне використання потоків) передбачає повторного вступу.

Ні безпечний потік, ні повторний учасник нічого не можуть сказати про аргументи: ми говоримо про одночасне виконання функції, яке все ще може бути небезпечним, якщо використовуються невідповідні параметри.

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

Важливо розуміти, що взагалі нісенітниця, щоб безпечна робота з потоками включала параметри. Якщо ви зробили будь-яке програмування баз даних, ви зрозумієте. Концепція того, що є "атомним" і може бути захищено мютекс або іншою технікою, обов'язково є концепцією користувача: обробка транзакції в базі даних може вимагати декількох безперебійних модифікацій. Хто може сказати, які з них потрібно підтримувати синхронізовано, але клієнтський програміст?

Справа в тому, що "корупція" не повинна псувати пам'ять на вашому комп'ютері з несеріалізованими записами: корупція все ще може виникнути, навіть якщо всі окремі операції серіалізовані. Звідси випливає, що коли ви запитуєте, чи функція є безпечною для потоків чи повторним входом, питання означає для всіх належним чином розділених аргументів: використання зв'язаних аргументів не є протилежним прикладом.

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

Хороший приклад - малок. Він не є реєстрантом і не є безпечним для потоків. Це відбувається тому, що він має доступ до глобального ресурсу (купи). Використання замків не робить його безпечним: це, безумовно, не повторно. Якщо інтерфейс до malloc був правильно розроблений, можна було б зробити його повторним і безпечним для потоків:

malloc(heap*, size_t);

Тепер це може бути безпечним, оскільки він передає відповідальність за серіалізацію спільного доступу до однієї купи на клієнта. Зокрема, робота не потрібна, якщо є окремі об’єкти купи. Якщо використовується загальна купа, клієнт повинен серіалізувати доступ. Використання блокування всередині функції недостатньо: просто розгляньте malloc, що блокує купу *, а потім подається сигнал і викликає malloc по тому самому вказівнику: тупиковий сигнал: сигнал не може продовжуватися, і клієнт не може, тому що це переривається.

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


"Тому одночасне безпечне (як правило, письмове безпечне використання потоків) передбачає повторного вступу." Це суперечить прикладу Вікіпедії "безпечно для потоків, але не є" .
Маджієро

3

Серед перелічених пунктів "загальний потік" (призначений каламбур!) Полягає в тому, що функція не повинна робити нічого, що впливало б на поведінку будь-яких рекурсивних або одночасних викликів до тієї ж функції.

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

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

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

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


1

Відповіді на ваші запитання "Також" - "Ні", "Ні" та "Ні". Просто тому, що функція є рекурсивною та / або безпечною для потоків, вона не робить її повторною вступкою.

Кожен з цих типів функцій може вийти з ладу у всіх пунктах, які ви цитуєте. (Хоча я не впевнений на 100% у пункті 5).


1

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

"Безпечний" тут, безумовно, не означає безпечний в ширшому розумінні, що виклик заданої функції в заданому контексті не буде повністю шлангом вашої програми. Загалом, функція може надійно створювати бажаний ефект у вашій багатопотоковій програмі, але не може бути визначеною як повторна або безпечна для потоків згідно з визначеннями. Навпаки, ви можете викликати функції повторного вступу таким чином, щоб створити різноманітні небажані, несподівані та / або непередбачувані ефекти у вашому багатопотоковому додатку.

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

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

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

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