Что именно является реентерабельной функцией?

198

Большинство из тех времен , определение reentrance цитата из Википедии :

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

  1. Не должен содержать статических (или глобальных) непостоянных данных.
  2. Не должен возвращать адрес статическим (или глобальным) непостоянным данным.
  3. Должен работать только на данных, предоставленных ему вызывающей стороной.
  4. Не должен полагаться на блокировки одноэлементных ресурсов.
  5. Не должен изменять свой собственный код (если только он не выполняется в своем собственном уникальном хранилище потоков)
  6. Не должны вызывать не входящие компьютерные программы или процедуры.

Как безопасно определить?

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

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

Также,

  1. Все рекурсивные функции реентерабельны?
  2. Все поточно-ориентированные функции реентерабельны?
  3. Все рекурсивные и поточно-ориентированные функции реентерабельны?

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

Lazer
источник
6
На самом деле, я не согласен с № 2 в первом списке. Вы можете вернуть адрес по своему усмотрению из функции повторного входа - ограничение состоит в том, что вы делаете с этим адресом в вызывающем коде.
2
@Neil Но так как автор реентерабельной функции не может контролировать то, что вызывающий объект наверняка не должен возвращать адрес статическим (или глобальным) непостоянным данным, чтобы он был действительно реентерабельным?
Robben_Ford_Fan_boy
2
@drelihan Автор ЛЮБОЙ функции (повторно входящей или нет) не несет ответственности за управление действиями вызывающей стороны с возвращаемым значением. Они, конечно, должны сказать, что звонящий МОЖЕТ сделать с этим, но если звонящий решит сделать что-то еще - неудача для звонящего.
«потокобезопасный» не имеет смысла, если только вы не укажете, что делают потоки и каков ожидаемый эффект их действий. Но, возможно, это должен быть отдельный вопрос.
Я могу с уверенностью сказать, что поведение четко определено и детерминировано независимо от расписания.
AturSams

Ответы:

191

1. Как это безопасно определить?

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

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

Нет.

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

#include <mutex>

typedef void (*callback)();
std::mutex m;

void foo(callback f)
{
    m.lock();
    // use the resource protected by the mutex

    if (f) {
        f();
    }

    // use the resource protected by the mutex
    m.unlock();
}

Другая функция вполне может потребоваться для блокировки того же мьютекса:

void bar()
{
    foo(nullptr);
}

На первый взгляд все вроде нормально… Но подождите:

int main()
{
    foo(bar);
    return 0;
}

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

  1. mainпозвоню foo.
  2. foo получит замок.
  3. fooпозвонит bar, который позвонит foo.
  4. 2-й foo попытается получить блокировку, выйти из строя и дождаться ее освобождения.
  5. Тупик.
  6. К сожалению ...

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

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

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

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

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

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

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

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

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

Нет.

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

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

4.2. Все поточно-ориентированные функции реентерабельны?

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

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

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

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

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

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

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

6. Пример

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

struct MyStruct
{
    P * p;

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

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

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

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

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

#include <mutex>

struct MyStruct
{
    std::recursive_mutex m;
    size_t c;
    P * p;

    void foo()
    {
        m.lock();

        if (c == 0)
        {
            this->p = new P();
        }

        ++c;
        m.unlock();
        // lots of code, some using this->p
        m.lock();
        --c;

        if (c == 0)
        {
            delete this->p;
            this->p = nullptr;
        }

        m.unlock();
    }
};

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

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

7. Привет 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;
    }

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

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

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

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

7,5. Убедитесь, что ваш потокобезопасный код рекурсивно-безопасный

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

paercebal
источник
1
Чтобы немного поразмыслить, я на самом деле думаю, что в этом случае "безопасность" определена - это означает, что функция будет действовать только на предоставленные переменные - т. Е. Это сокращение от кавычки определения ниже. И дело в том, что это не может подразумевать другие идеи безопасности.
Джо, приносящий душ
Вы пропустили переход в мьютекс в первом примере?
10.10
@paercebal: твой пример неверен. На самом деле вам не нужно беспокоиться об обратном вызове, простая рекурсия будет иметь ту же проблему, если она есть, но единственная проблема - вы забыли точно сказать, где расположена блокировка.
Иттрил
3
@Yttrill: я полагаю, вы говорите о первом примере. Я использовал «обратный вызов», потому что, по сути, пахнет обратный вызов. Конечно, рекурсивная функция имела бы ту же проблему, но обычно можно легко проанализировать функцию и ее рекурсивную природу, таким образом, определить, является ли она реентерабельной или подходит ли она для рекурсивности. Обратный вызов, с другой стороны, означает, что автор функции, вызывающей обратный вызов, не имеет никакой информации о том, что делает обратный вызов, поэтому этому автору может быть трудно убедиться, что его / ее функция реентерабельна. Это та сложность, которую я хотел показать.
paercebal
1
@Gab 是 好人: я исправил первый пример. Спасибо! Обработчик сигнала будет иметь свои собственные проблемы, отличные от повторного входа, как обычно, когда сигнал повышается, вы не можете ничего сделать, кроме изменения специально объявленной глобальной переменной.
paercebal
21

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

Ответы на ваши 3 вопроса - 3 × «нет».


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

НЕТ!

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


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

НЕТ!

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


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

НЕТ!

Смотри выше.

бездельник
источник
10

Общая нить:

Хорошо ли определено поведение, если подпрограмма вызывается во время ее прерывания?

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

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

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

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

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

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

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

drawnonward
источник
7

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

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

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

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

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

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

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

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

malloc(heap*, size_t);

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

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

Yttrill
источник
«Таким образом, параллельная безопасность (обычно написанная как потокобезопасная) подразумевает повторное поступление». Это противоречит «векторизованному, но не реентерабельному» примеру Википедии .
Мэггеро
3

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

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

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

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

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

Клиффорд
источник
1

Ответы на ваши вопросы «Также»: «Нет», «Нет» и «Нет». Только потому, что функция рекурсивна и / или поточно-ориентирована, она не делает ее входной.

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

ChrisF
источник
1

Термины «потокобезопасный» и «повторно входящий» означают только и именно то, что говорят их определения. «Безопасный» в данном контексте означает только то, что говорится в приведенном ниже определении.

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

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

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

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

Джо Приносящий Душу
источник