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

большинство of на времени определение однако это цитата из Википедия:

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

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

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

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

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

и

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

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

7 ответов


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

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

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

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

нет.

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

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

4.2. Все потокобезопасные функции reentrant?

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

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

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

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

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

5. Являются ли термины, такие как reentrance и безопасность резьбы, абсолютными вообще, т. е. имеют ли они фиксированный бетон definations?

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

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

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

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

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


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

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


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

нет!

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


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

нет!

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


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

нет!

см. выше.


общий поток:

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

malloc(heap*, size_t);

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

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


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

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

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

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

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


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

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


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

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

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

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

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