Повторный вход в код против безопасности потоков

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

Реентерабельный и потокобезопасный код

Я не мог ясно понять объяснение. Помощь приветствуется.


person Codex    schedule 09.12.2008    source источник


Ответы (2)


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

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

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

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

person ConcernedOfTunbridgeWells    schedule 09.12.2008
comment
Ваш второй пример не кажется мне повторным. Если изменение можно прервать, оставив несогласованное состояние, и в этот момент возникает сигнал, а обработчик этого сигнала вызывает функцию, то обычно происходит бум. Это проблема повторного входа, а не проблема безопасности потоков. - person Steve Jessop; 09.12.2008
comment
Вы правы - как вы говорите ниже, вам также придется отключить сигналы, чтобы последний пример был эффективно реентерабельным. - person ConcernedOfTunbridgeWells; 09.12.2008
comment
@ConcernedOfTunbridgeWells, если функция использует кучу внутри, есть большая вероятность, что эта функция не является повторной. Почему? - person Alcott; 21.09.2011
comment
@Alcott - если функция использует исключительно переменные, размещенные в стеке, то каждый вызов будет создавать новый кадр стека, поэтому общего состояния нет. Если куча содержит общую структуру данных, используемую функцией, то конфликтующие вызовы потенциально могут повредить общую структуру данных, если только вокруг нее не размещен какой-либо механизм защиты (например, мьютекс). - person ConcernedOfTunbridgeWells; 21.09.2011

В этой статье говорится:

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

Там также говорится:

«Нереентерабельные функции небезопасны для потоков».

Я вижу, как это может вызвать путаницу. Они означают, что стандартные функции, задокументированные как не требующие повторного входа, также не обязаны быть потокобезопасными, что справедливо для библиотек POSIX iirc (и POSIX объявляет это истинным также и для библиотек ANSI/ISO, ISO имеет нет концепции потоков и, следовательно, нет концепции потокобезопасности). Другими словами, «если функция говорит, что она не реентерабельна, то она также говорит, что она небезопасна для потоков». Это не логическая необходимость, это просто условность.

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

take_global_lock();
int i = get_global_counter();
do_callback(i);
set_global_counter(i+1);
release_global_lock();

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

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

Вот некоторый псевдокод, который является "слабо реентерабельным", но не потокобезопасным:

int i = get_global_counter();
do_callback(i);
set_global_counter(get_global_counter()+1);

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

Вот некоторый код, который, возможно, полностью реентерабельный (за исключением того, что я думаю, что стандарт различает реентерабельный и «непрерываемый сигналами», и я не уверен, где это падает), но все еще не является потокобезопасным:

int i = get_global_counter();
do_callback(i);
disable_signals(); // and any other kind of interrupts on your system
set_global_counter(get_global_counter()+1);
restore_signal_state();

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

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

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

person Steve Jessop    schedule 09.12.2008