Почему этот код реентерабелен, но не потокобезопасен

Раньше я думал, что все реентеративные функции являются потокобезопасными. Но я прочитал страницу Reentrancy в Wiki, он отправляет код, который является "совершенно реентерабельным, но не потокобезопасным, потому что он не обеспечивает глобальные данные находится в согласованном состоянии во время выполнения"

int t;

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

        s = t;  // save global variable
        t = *x;
        *x = *y;
        // hardware interrupt might invoke isr() here!
        *y = t;
        t = s;  // restore global variable
}

void isr()
{
        int x = 1, y = 2;
        swap(&x, &y);
}

Я не понимаю его объяснения. Почему эта функция не является потокобезопасной? Это потому, что глобальная переменная int t будет изменена при выполнении потоков?

Ответы

Ответ 1

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

В приведенном примере глобальная переменная не изменяется между двумя вызовами функции. То, что происходит внутри функции, не влияет на каждый вызов функции.

Примером невозвратной функции является strtok

Он, например, не может вложить в него две пары синтаксического анализа:

 /* To read a several lines of comma separated numbers */
 char buff[WHATEVER], *p1, *p2;

  p1 = strtok(buff, "\n");
  while(p1) {
    p2 = strtok(p1, ",");
    while(p2) {
      atoi(p2);
      p2 = strtok(NULL, ",");
      }
    }
    p1 = strtok(NULL, "\n");
  }

Это не работает, потому что состояние внешнего цикла strtok сбивается вторым вызовом (нужно использовать вариант реентера strtok_r).

Ответ 2

Трюк с этим типом повторного запуска состоит в том, что выполнение первого вызова прекращается, пока выполняется второй вызов. Также как вызов подфункции. Первый вызов продолжается после завершения второго вызова. Поскольку функция сохраняет состояние t при входе и восстанавливает его при выходе, ничто не изменилось для первого вызова, когда оно продолжается. Поэтому у вас всегда есть определенный и строгий порядок выполнения, независимо от того, где именно прерван первый вызов.

Когда эта функция работает в нескольких потоках, все исполнения выполняются параллельно, даже в истинной параллели с многоядерным процессором. Нет определенного порядка выполнения по всем потокам, только в пределах одного потока. Таким образом, значение t может быть изменено в любой момент одним из других потоков.

Ответ 3

Предположим, что Thread A и Thread B. Thread A имеет две локальные переменные a = 5, b = 10, а Thread B имеет две локальные переменные p = 20, q = 30.

Потоки вызовов A: swap (& a, & b);

Потоки B: swap (& p, & q);

Я предполагаю, что оба потока работают на разных ядрах и принадлежат к одному и тому же процессу. Переменная t является глобальной и int x, int y являются локальными для данной функции. Следующее планирование потоков показывает, как значение "t" может меняться в зависимости от планирования потоков и, следовательно, небезопасно. Скажем, глобальный t = 100;

 Thread A         Thread B
 1) int s;        int s;
 2) s = 100;      s = 100;
 3) t = 5;        no operation(nop);
 4) nop;          t = 20;  // t is global so Thread A also sees the value as t = 20
 5) x = 10;       x = 30;
 6) y = 20;       y = 20;  // Thread A exchange is wrong, Thread B exchange is OK  

Теперь попробуйте представить, что могло бы произойти, если утверждения 3 и 4 находятся в другом порядке выше. t будет получать значение 5 и обмен в потоке B будет неправильным. Ситуация еще проще, если два потока находятся на одном процессоре. Тогда ни одна из вышеперечисленных операций не будет одновременной. Я только что показал чередование на шагах 3 и 4, поскольку они являются наиболее важными.

Ответ 4

Я попытаюсь предложить другой (возможно, менее надуманный) пример функции, которая является реентерабельной, но не потокобезопасной.

Вот реализация "Башни Ханоя", используя общий глобальный стек "temp":

stack_t tmp;

void hanoi_inner(stack_t src, stack_t dest, stack_t tmp, int n)
{
   if (n == 1) move(src, dest)
   else {
     hanoi_inner(src, tmp, dest, n - 1);
     move(src, dest);
     hanoi_inner(tmp, dest, src, n - 1);
   }
}

void hanoi(stack_t src, stack_t dest, int n) { hanoi_inner(src, dest, tmp, n); }

Функция hanoi() является реентерабельной, поскольку она оставляет состояние глобального буфера tmp неизменным при его возврате (одно предупреждение: обычное ограничение на увеличение размера дисков на tmp может быть нарушено во время реентерабера call.) Однако hanoi() не является потокобезопасным.

Вот пример, который является как потокобезопасным, так и реентерабельным, если оператор приращения n++ является атомарным:

int buf[MAX_SIZE];  /* global, shared buffer structure */
int n;              /* global, shared counter */

int* alloc_int() { return &buf[n++]; }

Вы действительно могли бы использовать это как распределитель для целых ячеек (не проверяйте переполнение, я знаю). Если n++ не является атомарной операцией, два потока или два реентерабельных вызова могут в конечном итоге быть выделены одной и той же ячейкой.

Ответ 5

Если у вас было 2 экземпляра (каждый в другом потоке), выполняющих его, можно было бы наступить на другие пальцы: если кто-то прервал комментарий "аппаратное прерывание", а другой выполнил, он мог бы изменить t, так что переключение Вернемся к первому, если бы он произвел неверные результаты.

Ответ 6

Таким образом, функция messes с глобальной переменной t называется некоторой причудливой причиной. Если эта функция вызывается из двух разных потоков одновременно, возможно, что вы получите неожиданные неправильные результаты, потому что один экземпляр перезапишет значение в t, которое было написано другим экземпляром.