Threadsafe проти повторного вступу


89

Нещодавно я задав запитання із заголовком "Чи безпечна нитка malloc?" , і всередині цього я запитав: "Malloc є повторним учасником?"

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

Чи є це припущення помилковим?

Відповіді:


42

Функції повторного вступу не покладаються на глобальні змінні, які виставляються в заголовках бібліотеки C. .. візьмемо strtok () проти strtok_r (), наприклад, у C.

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

errno, однак, дещо інший випадок у системах POSIX (і, як правило, є дивним у будь-якому поясненні того, як все це працює) :)

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

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

Функціями, які повертають статично розподілені значення, є не безпечними для потоку без використання mutex, futex або іншого атомного механізму блокування. Тим не менше, їм не потрібно повертатися, якщо їх не збираються переривати.

тобто:

static char *foo(unsigned int flags)
{
  static char ret[2] = { 0 };

  if (flags & FOO_BAR)
    ret[0] = 'c';
  else if (flags & BAR_FOO)
    ret[0] = 'd';
  else
    ret[0] = 'e';

  ret[1] = 'A';

  return ret;
}

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

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

Кращий спосіб забезпечити безпеку потокового зв'язку для одночасного доступу , що краще ілюструє потребу.


2
Реентрант не означає безпеку потоку. Чисті функції передбачають безпеку різьблення.
Хуліо Герра,

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

@ Tim Post "Коротше кажучи, реентерант часто означає безпечний для потоку (як у" використовуйте версію цієї функції, що використовує ретранслятор, якщо ви використовуєте потоки "), але безпечний для потоку не завжди означає повторного входу." qt говорить протилежне: "Отже, потокобезпечна функція завжди є реентерабельною, але функція реентрантної не завжди безпечна для потоків."
4pie0

а wikipedia говорить ще про щось інше: "Це визначення повторного входу відрізняється від визначення захищеності потоків у багатопотокових середовищах. Підпрограмний перехід, що входить, може забезпечити безпеку потоків, [1] у будь-яких ситуаціях. І навпаки, захищений від потоків код не обов'язково повинен бути перенаправлений (...) "
4pie0,

@Riccardo: Функції, синхронізовані за допомогою мінливих змінних, але не повних бар'єрів пам'яті для використання з обробниками сигналів / переривань, як правило, повторно входять, але безпечні для потоку.
doynax

77

TL; DR: функція може бути перенаправлена, захищена від потоків, як одна, так і інша.

Статті Вікіпедії про безпеку потоків та повернення варто прочитати. Ось кілька цитат:

Функція захищена від потоку, якщо:

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

Функція повертається, якщо:

його можна перервати в будь-який момент під час його виконання, а потім безпечно викликати знову ("повторно ввести") до завершення попереднього виклику.

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

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

Приклади

(Трохи змінено зі статей Вікіпедії)

Приклад 1: не захищений від потоків, не повертається

/* As this function uses a non-const global variable without
   any precaution, it is neither reentrant nor thread-safe. */

int t;

void swap(int *x, int *y)
{
    t = *x;
    *x = *y;
    *y = t;
}

Приклад 2: безпечний для потоку, не повертається

/* We use a thread local variable: the function is now
   thread-safe but still not reentrant (within the
   same thread). */

__thread int t;

void swap(int *x, int *y)
{
    t = *x;
    *x = *y;
    *y = t;
}

Приклад 3: не захищений від потоків, повертається

/* We save the global state in a local variable and we restore
   it at the end of the function.  The function is now reentrant
   but it is not thread safe. */

int t;

void swap(int *x, int *y)
{
    int s;
    s = t;
    t = *x;
    *x = *y;
    *y = t;
    t = s;
}

Приклад 4: ниткобезпечний, реентерабельний

/* We use a local variable: the function is now
   thread-safe and reentrant, we have ascended to
   higher plane of existence.  */

void swap(int *x, int *y)
{
    int t;
    t = *x;
    *x = *y;
    *y = t;
}

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

11
Мені здається, що приклад 3 не повертається: якщо обробник сигналу, перериваючи після t = *x, дзвінки swap(), tбуде замінений, що призведе до несподіваних результатів.
rom1v

1
@ SandBag_1996, давайте розглянемо виклик, swap(5, 6)який перериває a swap(1, 2). Після t=*x, s=t_originalі t=5. Тепер, після переривання, s=5і t=1. Однак перед другим swapповерненням він відновить контекст, створення t=s=5. Тепер ми повернемося до першого swapз t=5 and s=t_originalі продовжимо після t=*x. Отже, функція, здається, повторно входить. Пам'ятайте, що кожен дзвінок отримує власну копію, sвиділену в стеку.
urnonav

4
@ SandBag_1996 Припущення полягає в тому, що якщо функція переривається (у будь-який момент), її потрібно викликати ще раз, і ми чекаємо, поки вона завершиться, перш ніж продовжувати вихідний виклик. Якщо щось інше трапляється, це в основному багатопотоковість, і ця функція не є безпечною для потоків. Припустимо, функція робить ABCD, ми приймаємо лише такі речі, як AB_ABCD_CD, або A_ABCD_BCD, або навіть A__AB_ABCD_CD__BCD. Як ви можете перевірити, приклад 3 добре працював би за цих припущень, тому він є ретрансляційним. Сподіваюся, це допомагає.
MiniQuark

1
@ SandBag_1996, мьютекс насправді зробив би його нереєнтарним. Перший виклик блокує мьютекс. Настає друге виклик - тупик.
urnonav

56

Це залежить від визначення. Наприклад, Qt використовує наступне:

  • Потокобезпечну * функцію можна одночасно викликати з декількох потоків, навіть коли для викликів використовуються спільні дані, оскільки всі посилання на спільні дані серіалізовані.

  • Поворотна функція також може викликатися одночасно з декількох потоків, але тільки тоді , коли кожен виклик використовує свої власні дані.

Отже, потокобезпечна функція завжди є ретрансляційною, але функція реентрантної не завжди є поточно безпечною.

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

але вони також застерігають:

Примітка. Термінологія в багатопотоковому домені не є повністю стандартизованою. POSIX використовує визначення реентерабельного та потокобезпечного, які дещо відрізняються для своїх API C. Використовуючи інші об'єктно-орієнтовані бібліотеки класів C ++ з Qt, переконайтеся, що визначення зрозумілі.


2
Це визначення реентанта занадто сильне.
qweruiop

Функція є як реентерабельною, так і безпечною для потоків, якщо не використовує жодних глобальних / статичних змінних. Потік - безпечно: коли багато потоків запускають вашу функцію одночасно, чи є якась гонка ?? Якщо ви використовуєте глобальний var, використовуйте блокування, щоб захистити його. тому він безпечний для потоків. reentrant: якщо під час виконання вашої функції виникає сигнал і знову викликається ваша функція в сигналі, це безпечно ??? у такому випадку немає кількох потоків. Краще, щоб ви не використовували жодних статичних / глобальних змінних, щоб повернути його, або як у прикладі 3.
keniee van
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.