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
。
- 第二个
foo
将尝试获取锁定,失败并等待释放。
- 僵局。
- 糟糕...
好的,我使用回调函数作弊。但是,很容易想象更复杂的代码片段会产生相似的效果。
3.在检查代码中的可重入功能时,应牢记的六个要点之间的共同点到底是什么?
你可以闻到,如果你的函数/可以访问一个可修改的持久性资源,或有/给予一个函数访问的问题气味。
(好吧,我们的代码中有99%应该有气味,然后……参见上一节以解决该问题……)
因此,研究您的代码时,其中之一应提醒您:
- 该函数具有状态(即访问全局变量,甚至是类成员变量)
- 该函数可以由多个线程调用,也可以在进程执行时在堆栈中出现两次(即该函数可以直接或间接调用自身)。以回调为参数的函数闻起来很多。
请注意,不可重入是病毒式的:可以调用可能的不可重入函数的函数不能视为可重入。
还要注意,C ++方法之所以臭味是因为它们可以访问this
,所以您应该研究代码以确保它们没有有趣的交互。
4.1。所有递归函数都是可重入的吗?
没有。
在多线程情况下,访问共享资源的递归函数可能会同时被多个线程调用,从而导致数据损坏/损坏。
在单线程情况下,递归函数可以使用非可重入函数(如infamous 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;
}
}
};
这样,代码就成为了递归安全的……但是由于多线程问题,它仍然是不可重入的:我们必须确保使用递归互斥锁对of c
和of 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
在两个线程之间共享a ,则用户必须使用并发原语保护其访问;
7.5。确保您的线程安全代码是递归安全的
这意味着如果您认为同一线程可以使用同一资源两次,则使用递归互斥锁。