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

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

Reentrant и потокобезопасный код

Я не был в состоянии понять объяснение ясно. Помощь будет оценена.

2 ответа

Решение

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

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

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

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

Эта статья гласит:

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

Это также говорит:

Msgstr "Не реентерабельные функции небезопасны".

Я вижу, как это может привести к путанице. Они означают, что стандартные функции, задокументированные как не требующие повторного входа, также не обязаны быть поточно-ориентированными, что справедливо для библиотек 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();

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

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

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

Другие вопросы по тегам