Что такое точно повторно используемая функция?

Большинство из времена, определение повторной входимости заключается в кавычки из Википедии:

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

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

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

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

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

Кроме того,

  1. Действительно ли все рекурсивные функции повторно используемы?
  2. Действительно ли все ориентированные на многопотоковое исполнение функции повторно используемы?
  3. Повторно используемы все рекурсивные и ориентированные на многопотоковое исполнение функции?

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

190
задан Community 23 May 2017 в 02:02
поделиться

6 ответов

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. Являются ли вообще такие термины как reentrance и thread safety абсолютными, т.е. имеют ли они фиксированные конкретные определения?

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

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

И конечно, все это предполагает, что множество кода является реентерабельным, включая использование 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. Убедитесь, что ваш потокобезопасный код рекурсивно безопасен

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

184
ответ дан 23 November 2019 в 05:37
поделиться

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

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


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

НЕТ!

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


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

НЕТ!

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


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

НЕТ!

См. выше.

21
ответ дан 23 November 2019 в 05:37
поделиться

Общая тема:

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

Если у вас есть функция вроде этой:

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

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

Если у вас есть функция, подобная этой:

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

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

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

10
ответ дан 23 November 2019 в 05:37
поделиться

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

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

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

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

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

1
ответ дан 23 November 2019 в 05:37
поделиться

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

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

1
ответ дан 23 November 2019 в 05:37
поделиться

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

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

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

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

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

3
ответ дан 23 November 2019 в 05:37
поделиться
Другие вопросы по тегам:

Похожие вопросы: