Реентерабельность — это концепция программирования, при которой функция или подпрограмма может быть прервана и затем возобновлена до того, как она завершит выполнение. Это означает, что функция может быть вызвана снова до того, как она завершит свое предыдущее выполнение. Реентерабельный код разработан так, чтобы быть безопасным и предсказуемым, когда несколько экземпляров одной и той же функции вызываются одновременно или в быстрой последовательности. Компьютерная программа или подпрограмма называется реентерабельной, если несколько вызовов могут безопасно выполняться одновременно на нескольких процессорах или если в однопроцессорной системе ее выполнение может быть прервано , а новое выполнение может быть безопасно запущено (ее можно «повторно ввести»). Прерывание может быть вызвано внутренним действием, таким как переход или вызов, или внешним действием, таким как прерывание или сигнал , в отличие от рекурсии , где новые вызовы могут быть вызваны только внутренним вызовом.
Это определение берет свое начало в многопрограммных средах, где несколько процессов могут быть активны одновременно и где поток управления может быть прерван прерыванием и передан подпрограмме обслуживания прерываний (ISR) или подпрограмме «обработчика». Любая подпрограмма, используемая обработчиком, которая потенциально могла выполняться при срабатывании прерывания, должна быть реентерабельной. Аналогично, код, совместно используемый двумя процессорами, обращающимися к общим данным, должен быть реентерабельным. Часто подпрограммы, доступные через ядро операционной системы , не являются реентерабельными. Следовательно, подпрограммы обслуживания прерываний ограничены в действиях, которые они могут выполнять; например, они обычно ограничены в доступе к файловой системе , а иногда даже в выделении памяти .
Реентерабельность не является ни необходимой, ни достаточной для потокобезопасности в многопоточных средах. Другими словами, реентерабельная подпрограмма может быть потокобезопасной, [1], но не гарантирует, что она будет [ необходима цитата ] . И наоборот, потокобезопасный код не обязательно должен быть реентерабельным (см. примеры ниже).
Другие термины, используемые для реентерабельных программ, включают «разделяемый код». [2] Реентерабельные подпрограммы иногда помечаются в справочных материалах как «безопасные по сигналам». [3] Реентерабельные программы часто являются « чистыми процедурами».
Повторный вход — это не то же самое, что и идемпотентность , при которой функция может быть вызвана более одного раза, но при этом сгенерировать точно такой же вывод, как если бы она была вызвана только один раз. Вообще говоря, функция производит выходные данные на основе некоторых входных данных (хотя оба являются необязательными, в общем случае). К общим данным может получить доступ любая функция в любое время. Если данные могут быть изменены любой функцией (и ни одна из них не отслеживает эти изменения), нет гарантии для тех, кто разделяет данные, что эти данные такие же, как и в любое время до этого.
Данные имеют характеристику, называемую областью действия , которая описывает, где в программе могут использоваться данные. Область действия данных может быть либо глобальной (вне области действия любой функции и с неопределенным объемом), либо локальной (создается каждый раз при вызове функции и уничтожается при выходе).
Локальные данные не являются общими для каких-либо подпрограмм, повторно входящих или нет; поэтому они не влияют на повторный вход. Глобальные данные определяются вне функций и могут быть доступны более чем одной функции, либо в форме глобальных переменных (данные, общие для всех функций), либо как статические переменные (данные, общие для всех вызовов одной и той же функции). В объектно-ориентированном программировании глобальные данные определяются в области действия класса и могут быть закрытыми, что делает их доступными только для функций этого класса. Существует также концепция переменных экземпляра , когда переменная класса привязана к экземпляру класса. По этим причинам в объектно-ориентированном программировании это различие обычно сохраняется для данных, доступных вне класса (публичных), и для данных, независимых от экземпляров класса (статических).
Повторный вход отличается от потокобезопасности , но тесно связан с ней . Функция может быть потокобезопасной и при этом не повторной. Например, функция может быть обернута вокруг мьютекса ( что позволяет избежать проблем в многопоточных средах), но если эта функция используется в процедуре обслуживания прерываний, она может голодать в ожидании первого выполнения, чтобы освободить мьютекс. Ключ к избежанию путаницы заключается в том, что повторный вход относится только к одному исполняемому потоку. Это концепция со времен, когда не существовало многозадачных операционных систем.
sig_atomic_t
эту цель, хотя и с гарантиями только для простых чтений и записей, а не для увеличения или уменьшения. [5] Более сложные атомарные операции доступны в C11 , который обеспечивает stdatomic.h
.Однако он может модифицировать себя, если он находится в своей собственной уникальной памяти. То есть, если каждый новый вызов использует другое физическое расположение машинного кода, где создается копия исходного кода, он не повлияет на другие вызовы, даже если он модифицирует себя во время выполнения этого конкретного вызова (потока).
Повторный вход подпрограммы, которая работает с ресурсами операционной системы или нелокальными данными, зависит от атомарности соответствующих операций. Например, если подпрограмма изменяет 64-битную глобальную переменную на 32-битной машине, операция может быть разделена на две 32-битные операции, и, таким образом, если подпрограмма прерывается во время выполнения и вызывается снова из обработчика прерываний, глобальная переменная может находиться в состоянии, в котором обновлены только 32 бита. Язык программирования может предоставлять гарантии атомарности для прерывания, вызванного внутренним действием, таким как переход или вызов. Тогда функция f
в выражении типа (global:=1) + (f())
, где порядок оценки подвыражений может быть произвольным в языке программирования, увидит, что глобальная переменная либо установлена в 1, либо в ее предыдущее значение, но не в промежуточном состоянии, в котором обновлена только часть. (Последнее может произойти в C , поскольку выражение не имеет точки последовательности .) Операционная система может предоставлять гарантии атомарности для сигналов , например, системный вызов, прерванный сигналом, не имеющий частичного эффекта. Аппаратное обеспечение процессора может предоставлять гарантии атомарности для прерываний , например, прерванные инструкции процессора, не имеющие частичного эффекта.
Для иллюстрации реентерабельности в данной статье в качестве примера используется служебная функция языка Cswap()
, которая берет два указателя и транспонирует их значения, а также процедура обработки прерываний, которая также вызывает функцию подкачки.
Это пример функции подкачки, которая не может быть реентерабельной или потокобезопасной. Поскольку переменная tmp
глобально распределена, без синхронизации, среди любых параллельных экземпляров функции, один экземпляр может вмешиваться в данные, на которые полагается другой. Таким образом, ее не следовало использовать в процедуре обслуживания прерывания isr()
:
int tmp ; void swap ( int * x , int * y ) { tmp = * x ; * x = * y ; /* Аппаратное прерывание может вызвать isr() здесь. */ * y = tmp ; } void isr () { int x = 1 , y = 2 ; поменять местами ( & x , & y ); }
Функцию swap()
в предыдущем примере можно сделать потокобезопасной, сделав ее tmp
thread-local . Она по-прежнему не может быть реентерабельной, и это будет продолжать вызывать проблемы, если isr()
она вызывается в том же контексте, что и поток, который уже выполняется swap()
:
_Thread_local int tmp ; void swap ( int * x , int * y ) { tmp = * x ; * x = * y ; /* Аппаратное прерывание может вызвать isr() здесь. */ * y = tmp ; } void isr () { int x = 1 , y = 2 ; поменять местами ( & x , & y ); }
Реализация, swap()
которая выделяет память tmp
в стеке, а не глобально, и которая вызывается только с неразделяемыми переменными в качестве параметров [b], является как потокобезопасной, так и реентерабельной. Потокобезопасной, поскольку стек является локальным для потока, а функция, действующая только на локальные данные, всегда будет выдавать ожидаемый результат. Нет доступа к разделяемым данным, поэтому нет гонки данных.
void swap ( int * x , int * y ) { int tmp ; tmp = * x ; * x = * y ; * y = tmp ; /* Аппаратное прерывание может вызвать isr() здесь. */ } void isr () { int x = 1 , y = 2 ; поменять местами ( & x , & y ); }
Реентерабельный обработчик прерываний — это обработчик прерываний , который повторно включает прерывания на ранней стадии в обработчике прерываний. Это может сократить задержку прерывания . [6] В общем, при программировании процедур обслуживания прерываний рекомендуется повторно включать прерывания как можно скорее в обработчике прерываний. Такая практика помогает избежать потери прерываний. [7]
В следующем коде ни f
одна из g
функций не является реентерабельной.
int v = 1 ; int f () { v += 2 ; return v ; } int g () { return f () + 2 ; }
В приведенном выше примере f()
зависит от неконстантной глобальной переменной v
; таким образом, если f()
во время выполнения прерывается ISR, который изменяет v
, то повторный вход в f()
вернет неправильное значение v
. Значение v
и, следовательно, возвращаемое значение f
, нельзя предсказать с уверенностью: они будут различаться в зависимости от того, было ли изменено прерывание v
во время f
выполнения . Следовательно, f
не является реентерабельным. Не является и g
, поскольку он вызывает f
, который не является реентерабельным.
Эти слегка измененные версии являются реентерабельными:
int f ( int i ) { return i + 2 ; } int g ( int i ) { return f ( i ) + 2 ; }
В следующем примере функция потокобезопасна, но не (обязательно) реентерабельна:
функция int () { mutex_lock (); // ... // тело функции // ... мьютекс_разблокировать (); }
В приведенном выше примере function()
может вызываться разными потоками без каких-либо проблем. Но если функция используется в реентерабельном обработчике прерываний и внутри функции возникает второе прерывание, вторая процедура зависнет навсегда. Поскольку обслуживание прерываний может отключить другие прерывания, может пострадать вся система.