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 не є рекурсивним, то ось що буде в основному потоці:
main
подзвонить foo
.
foo
придбає замок.
foo
подзвонить bar
, який зателефонує foo
.
- 2-й
foo
спробує придбати замок, вийде з ладу і чекати його звільнення.
- Тупик.
- На жаль ...
Гаразд, я обдурив, використовуючи річ зворотного дзвінка. Але легко уявити складніші фрагменти коду, що мають подібний ефект.
3. Що саме є загальним рядком між згаданими шістьма пунктами, про які я повинен пам’ятати, перевіряючи код на наявність можливостей рецензії?
Ви можете відчути проблему, якщо ваша функція має / надає доступ до змінного постійного ресурсу або має / надає доступ до функції, яка пахне .
( Гаразд, 99% нашого коду повинно пахнути, то ... Дивіться останній розділ, щоб вирішити це… )
Отже, вивчаючи свій код, один із цих моментів повинен вас насторожити:
- Функція має стан (тобто доступ до глобальної змінної або навіть змінної члена класу)
- Цю функцію можна викликати декількома потоками, або вона може з’являтися двічі у стеці під час виконання процесу (тобто функція може викликати себе безпосередньо чи опосередковано). Функція приймає зворотні виклики як параметри сильно пахнуть .
Зауважте, що невідповідність є вірусною: Функція, яка могла викликати можливу функцію, яка не є ререрантом, не може вважатися ретентованною.
Зауважте, що методи 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. Переконайтесь, що ваш захищений від потоку код є рекурсивно-безпечним
Це означає використовувати рекурсивні мютекси, якщо ви вважаєте, що один і той же потік можна використовувати один і той же ресурс двічі.