Что такое функция реентерабера?

Большинство , определение reentrance приведено в Wikipedia:

Компьютерная программа или подпрограмма описанный как повторный, если он может быть безопасно снова вызван предыдущий вызов завершен (то есть его можно безопасно выполнить одновременно). Чтобы быть реентерабельной, компьютерная программа или подпрограмма:

  • Не должно содержать статических (или глобальных) непостоянные данные.
  • Нельзя вернуть адрес статическая (или глобальная) непостоянная данные.
  • Должен работать только по предоставленным данным к нему вызывающим.
  • Нельзя полагаться на блокировки для singleton Ресурсы.
  • Не следует изменять собственный код (кроме выполняя свою собственную уникальную нить хранилище)
  • Нельзя вызывать нерентабельный компьютер программ или подпрограмм.

Как безопасно безопасно?

Если программа может быть безопасно выполнена одновременно, всегда ли она означает, что она реентерабельна?

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

Кроме того,

  • Все рекурсивные функции реентерабельны?
  • Все ли потокобезопасные функции реентерабельны?
  • Рекурсивные и потокобезопасные функции реентерабельны?

При написании этого вопроса приходит в голову следующее: Являются ли такие термины как reentrance и безопасностью потоков абсолютными, т.е. Имеют ли они конкретные конкретные определения? Ибо, если это не так, этот вопрос не имеет особого значения.

Ответ 1

1. Как безопасно безопасно?

семантически. В этом случае это не определенный термин. Это просто означает, что вы можете сделать это без риска ".

2. Если программа может быть безопасно выполнена одновременно, всегда ли она означает, что она является реентерабельной?

Нет.

Например, пусть имеет функцию С++, которая принимает как блокировку, так и обратный вызов в качестве параметра:

typedef void (*MyCallback)() ;
NonRecursiveMutex mutex ;

void myFunction(MyCallback f)
{
   lock(mutex) ;
   f() ;
   unlock(mutex) ;
}

На первый взгляд эта функция кажется Ok... Но подождите:

int main(int argc, char * argv[])
{
   myFunction(myFunction) ;
   return 0 ;
}

Если блокировка на мьютексе не рекурсивна, то вот что произойдет:

  • main вызовет myFunction
  • myFunction получит блокировку
  • myFunction вызовет myFunction
  • второй myFunction попытается получить блокировку, потерпеть неудачу и дождаться ее выхода
  • Тупик.
  • Упс...

Хорошо, я обманул, используя вещь Обратного вызова. Но легко представить себе более сложные фрагменты кода, имеющие аналогичный эффект.

3. Что такое общий поток между шестью пунктами, о которых я должен помнить, проверяя мой код на возможности повторного входа?

Вы можете запах проблемы, если ваша функция имеет/предоставляет доступ к изменяемому постоянному ресурсу или имеет/предоставляет доступ к функции, которая пахнет.

(Хорошо, 99% нашего кода должно пахнуть, затем... См. последний раздел для обработки этого...)

Итак, изучая свой код, один из этих пунктов должен предупредить вас:

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

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

Заметьте, что методы С++ запах, потому что они имеют доступ к this, поэтому вы должны изучить код, чтобы убедиться, что у них нет смешного взаимодействия.

4,1. Все рекурсивные функции реентерабельны?

Нет.

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

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

4,2. Все ли потокобезопасные функции реентерабельны?

В приведенном выше примере я показал, как явно небезопасная функция не была реентерабельной. Хорошо, я обманул из-за параметра Callback. Но тогда существует несколько способов блокировки потока, поскольку он получает дважды блокировку без повтора.

4,3. Рекурсивные и потокобезопасные функции реентерабельны?

Я бы сказал "да", если "рекурсивный" означает "рекурсивный-безопасный".

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

Проблема заключается в оценке этой гарантии... ^ _ ^

пять. Являются ли такие термины, как безопасность повторного входа и поточной безопасности абсолютным, то есть они фиксируют конкретные определения?

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

6. Пример

Скажем, у вас есть объект с одним методом, который должен использовать ресурсы:

struct MyStruct
{
   P * p ;

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

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

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

Первая проблема заключается в том, что если какая-то функция называется рекурсивно (т.е. эта функция вызывает себя, прямо или косвенно), код, вероятно, сбой, потому что 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 = NULL ;
      }
   }
} ;

Таким образом, код становится рекурсивным-безопасным... Но он по-прежнему не реентерабелен из-за многопоточных проблем: мы должны быть уверены, что модификации c и p будут выполняться атомарно, используя рекурсивный mutex (не все мьютексы являются рекурсивными):

struct MyStruct
{
   mutex   m ; // recursive mutex
   size_t  c ;
   P *     p ;

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

      ++c ;
      unlock(m) ;

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

      lock(m) ;
      --c ;

      if(c == 0)
      {
         delete this->p ;
         this->p = NULL ;
      }
      unlock(m) ;
   }
} ;

И, конечно же, все это предполагает, что lots of code сам является реентерабельным, включая использование p.

И вышеприведенный код даже не удаленно исключение-безопасный, но это еще одна история... ^ _ ^

7. Hey 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 ;
   }

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

7.4. Убедитесь, что пользователь знает, что объект не является потокобезопасным

Таким образом, пользователь должен использовать мьютексы для использования объекта, совместно используемого между потоками.

Объекты из STL предназначены для того, чтобы быть небезопасными (из-за проблем с производительностью), и, таким образом, если пользователь хочет разделить std::string между двумя потоками, пользователь должен защитить свой доступ с помощью concurrency примитивы;

7.5. Убедитесь, что код, защищенный потоком, является рекурсивным.

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

Ответ 2

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

Ответы на ваши 3 вопроса: 3 × "нет".


Все рекурсивные функции реентерабельны?

НЕТ!

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


Все ли потокобезопасные функции реентерабельны?

НЕТ!

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


Рекурсивные и потокобезопасные функции реентерабельны?

НЕТ!

См. выше.

Ответ 3

Общий поток:

Является ли поведение корректным, если процедура вызывается во время ее прерывания?

Если у вас есть такая функция:

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

Тогда он не зависит от какого-либо внешнего состояния. Поведение хорошо определено.

Если у вас есть такая функция:

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

Результат не определен для нескольких потоков. Информация может быть потеряна, если время было неправильным.

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

Ответ 4

Теперь я должен подробно остановиться на своем предыдущем комментарии. Ответ @paercebal неверен. В примере кода никто не заметил, что мьютекс, который, как предполагается, был параметром, фактически не передавался?

Я оспариваю заключение, утверждаю: для того, чтобы функция была в безопасности в присутствии concurrency, она должна быть повторной. Поэтому одновременная защита (обычно написанная поточно-безопасная) подразумевает повторный вход.

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

Например, memcpy() является потокобезопасным и повторным (обычно). Очевидно, что он не будет работать так, как ожидалось, если вызывается с указателями на одни и те же цели из двух разных потоков. То, что точка определения SGI, помещая бремя на клиенте для обеспечения доступа к той же структуре данных, синхронизируется клиентом.

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

Дело в том, что "коррупция" не должна испортить память на вашем компьютере с неэриализованной записью: коррупция все равно может произойти, даже если все отдельные операции сериализованы. Из этого следует, что когда вы спрашиваете, является ли функция потокобезопасной или повторной, вопрос означает для всех соответствующих разделенных аргументов: использование связанных аргументов не является контр-примером.

Существует много программных систем: Ocaml - один, и я думаю, что Python также имеет много нереарантного кода, но он использует глобальную блокировку для чередования потоков. Эти системы не являются повторными и не являются потокобезопасными или совместимыми друг с другом, они безопасно работают, потому что они предотвращают concurrency глобально.

Хорошим примером является malloc. Он не является повторным, а не потокобезопасным. Это связано с тем, что он должен получить доступ к глобальному ресурсу (куче). Использование блокировок не делает его безопасным: он определенно не является повторным. Если интерфейс для malloc был правильно спроектирован, можно было бы сделать его повторным и потокобезопасным:

malloc(heap*, size_t);

Теперь это может быть безопасно, поскольку оно передает ответственность за сериализацию совместного доступа к одной куче клиенту. В частности, никакая работа не требуется, если есть отдельные объекты кучи. Если используется общая куча, клиент должен сериализовать доступ. Используя блокировку внутри, функции недостаточно: просто подумайте о блокировке кучи malloc *, а затем идет сигнал и вызывает malloc на том же указателе: deadlock: сигнал не может продолжаться, и клиент не может либо потому, что он прерван.

Вообще говоря, блокировки не делают потоки безопасными. Они фактически уничтожают безопасность, ненадлежащим образом пытаясь управлять ресурсом, принадлежащим клиенту. Блокировка должна выполняться производителем объекта, это единственный код, который знает, сколько объектов создано и как они будут использоваться.

Ответ 5

"Общий поток" (pun предназначенный!?) среди перечисленных пунктов состоит в том, что функция не должна делать ничего, что могло бы повлиять на поведение любых рекурсивных или одновременных вызовов одной и той же функции.

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

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

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

Нить-безопасная функция не должна быть реентерабельной; он может обеспечить безопасность потока, специально предотвращая повторное включение с помощью замка, а точка (6) говорит, что такая функция не является повторной. Что касается точки (6), функция, которая вызывает защищенную потоком функцию, которая блокирует, небезопасна для использования в рекурсии (она будет заблокирована), и поэтому не считается реентерабельной, хотя она тем не менее может быть безопасна для concurrency, и будет по-прежнему быть повторным в том смысле, что несколько потоков могут иметь свои программные счетчики в такой функции одновременно (только не с заблокированной областью). Может быть, это помогает отличить потокобезопасность от повторного использования (или, может быть, добавляет к вашей путанице!).

Ответ 6

Ответы на ваши вопросы "Также": "Нет" , "Нет" и "Нет" . Просто потому, что функция рекурсивна и/или потокобезопасна, она не делает ее повторной.

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

Ответ 7

Термины "Thread-safe" и "re-entrant" означают только то, что говорят их определения. "Безопасный" в этом контексте означает только то, о чем говорится в приведенном ниже определении.

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

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

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

Многопоточное программирование просто чрезвычайно сложно в общем случае. Зная, какая часть одного повторителя кода является лишь частью этой задачи. Защита резьбы не является аддитивной. Вместо того, чтобы пытаться объединить функции повторного входа, лучше использовать общий поточно-безопасный шаблон дизайна и используйте этот шаблон, чтобы направлять ваше использование каждого потока и общих ресурсов в вашей программе.