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;
}
Если блокировка мьютекса не является рекурсивной, то вот что произойдет в основном потоке:
main
позвоню foo
.
foo
получит замок.
foo
позвонит bar
, который позвонит foo
.
- 2-й
foo
попытается получить блокировку, выйти из строя и дождаться ее освобождения.
- Тупик.
- К сожалению ...
Хорошо, я обманул, используя функцию обратного вызова. Но легко представить более сложные фрагменты кода, имеющие подобный эффект.
3. Что именно является общим потоком между шестью упомянутыми моментами, которые я должен помнить при проверке моего кода на возможность повторного входа?
Вы можете почувствовать проблему, если ваша функция имеет / дает доступ к изменяемому постоянному ресурсу или имеет / дает доступ к функции, которая пахнет .
( Хорошо, 99% нашего кода должно пахнуть, тогда ... Смотрите последний раздел, чтобы справиться с этим ... )
Итак, изучая ваш код, один из этих пунктов должен предупредить вас:
- Функция имеет состояние (то есть доступ к глобальной переменной или даже к переменной члена класса)
- Эта функция может вызываться несколькими потоками или может появляться дважды в стеке во время выполнения процесса (т. Е. Функция может вызывать сама, прямо или косвенно). Функция принимает обратные вызовы, так как параметры сильно пахнут .
Обратите внимание, что не входящий в систему вирусный: функция, которая может вызвать возможную не входящую функцию, не может считаться входящей.
Также обратите внимание, что методы 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. Убедитесь, что ваш потокобезопасный код рекурсивно-безопасный
Это означает использование рекурсивных мьютексов, если вы считаете, что один и тот же ресурс может дважды использоваться одним и тем же потоком.