問題描述
大多數 of 次,定義重入引用自維基百科:
Most of the times, the definition of reentrance is quoted from Wikipedia:
計算機程序或例程是如果可以,則描述為可重入的安全在它之前再次調用之前的調用已經完成(即它可以安全地執行同時).為了可重入,一個計算機程序或例程:
A computer program or routine is described as reentrant if it can be safely called again before its previous invocation has been completed (i.e it can be safely executed concurrently). To be reentrant, a computer program or routine:
- 不得持有靜態(或全局)非常量數據.
- 不得將地址返回給靜態(或全局)非常量數據.
- 必須只處理提供的數據由來電者提供.
- 不能依賴單例鎖資源.
- 不得修改自己的代碼(除非在它自己獨特的線程中執行存儲)
- 不得調用不可重入計算機程序或例程.
如何定義安全?
如果一個程序可以安全地并發執行,它是否總是意味著它是可重入的?
If a program can be safely executed concurrently, does it always mean that it is reentrant?
在檢查代碼的可重入功能時,我應該牢記的六點之間的共同點究竟是什么?
What exactly is the common thread between the six points mentioned that I should keep in mind while checking my code for reentrant capabilities?
還有,
- 所有遞歸函數都是可重入的嗎?
- 所有線程安全函數都是可重入的嗎?
- 所有遞歸函數和線程安全函數都是可重入的嗎?
在寫這個問題的時候,想到一件事:重入和線程安全之類的術語是否是絕對的,即它們是否有固定的具體定義?因為,如果不是,這個問題就沒有太大意義.
While writing this question, one thing comes to mind: Are the terms like reentrance and thread safety absolute at all i.e. do they have fixed concrete definitions? For, if they are not, this question is not very meaningful.
推薦答案
1.安全是如何定義的?
語義上.在這種情況下,這不是一個硬定義的術語.這只是意味著你可以做到,沒有風險".
1. How is safely defined?
Semantically. In this case, this is not a hard-defined term. It just mean "You can do that, without risk".
沒有
例如,讓我們有一個 C++ 函數,它將鎖和回調作為參數:
For example, let's have a C++ function that takes both a lock, and a callback as a parameter:
#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();
}
另一個函數很可能需要鎖定同一個互斥鎖:
Another function could well need to lock the same mutex:
void bar()
{
foo(nullptr);
}
乍一看,一切似乎都很好……但是等等:
At first sight, everything seems ok… But wait:
int main()
{
foo(bar);
return 0;
}
如果互斥鎖不是遞歸的,那么在主線程中會發生以下情況:
If the lock on mutex is not recursive, then here's what will happen, in the main thread:
main
將調用foo
.foo
將獲取鎖.foo
會調用bar
,后者會調用foo
.- 第二個
foo
將嘗試獲取鎖,失敗并等待它被釋放. - 僵局.
- 糟糕……
main
will callfoo
.foo
will acquire the lock.foo
will callbar
, which will callfoo
.- the 2nd
foo
will try to acquire the lock, fail and wait for it to be released. - Deadlock.
- Oops…
好吧,我欺騙了,使用回調的東西.但是很容易想象具有類似效果的更復雜的代碼段.
Ok, I cheated, using the callback thing. But it's easy to imagine more complex pieces of code having a similar effect.
如果您的函數具有/授予訪問可修改持久資源的權限,或者具有/授予訪問異味的功能,則您可以嗅到問題.
You can smell a problem if your function has/gives access to a modifiable persistent resource, or has/gives access to a function that smells.
(好吧,我們 99% 的代碼都應該有味道,然后……請參閱最后一節來處理那個……)
因此,在研究您的代碼時,其中一點應該提醒您:
So, studying your code, one of those points should alert you:
- 函數有狀態(即訪問全局變量,甚至類成員變量)
- 這個函數可以被多個線程調用,也可以在進程執行時在堆棧中出現兩次(即函數可以直接或間接調用自身).函數將回調作為參數氣味很多.
請注意,不可重入是病毒式的:可以調用可能的不可重入函數的函數不能被視為可重入.
Note that non-reentrancy is viral : A function that could call a possible non-reentrant function cannot be considered reentrant.
還要注意,C++ 方法氣味,因為它們可以訪問this
,因此您應該研究代碼以確保它們沒有有趣的交互.
Note, too, that C++ methods smell because they have access to this
, so you should study the code to be sure they have no funny interaction.
沒有
在多線程情況下,訪問共享資源的遞歸函數可能會同時被多個線程調用,從而導致數據錯誤/損壞.
In multithreaded cases, a recursive function accessing a shared resource could be called by multiple threads at the same moment, resulting in bad/corrupted data.
在單線程情況下,遞歸函數可以使用不可重入函數(如臭名昭著的strtok
),或者使用全局數據而不處理數據已經在使用的事實.所以你的函數是遞歸的,因為它直接或間接地調用自己,但它仍然可以遞歸不安全.
In singlethreaded cases, a recursive function could use a non-reentrant function (like the infamous strtok
), or use global data without handling the fact the data is already in use. So your function is recursive because it calls itself directly or indirectly, but it can still be recursive-unsafe.
在上面的例子中,我展示了一個明顯的線程安全函數是如何不可重入的.好吧,我因為回調參數而作弊.但是,有多種方法可以通過讓線程獲取兩次非遞歸鎖來死鎖線程.
In the example above, I showed how an apparently threadsafe function was not reentrant. OK, I cheated because of the callback parameter. But then, there are multiple ways to deadlock a thread by having it acquire twice a non-recursive lock.
如果遞歸"是指遞歸安全",我會說是".
I would say "yes" if by "recursive" you mean "recursive-safe".
如果你能保證一個函數可以被多個線程同時調用,并且可以直接或間接調用自己,沒有問題,那么它就是可重入的.
If you can guarantee that a function can be called simultaneously by multiple threads, and can call itself, directly or indirectly, without problems, then it is reentrant.
問題是評估這個保證……^_^
The problem is evaluating this guarantee… ^_^
我相信他們會這樣做,但是,評估一個函數是線程安全的還是可重入的可能很困難.這就是我使用上面氣味這個詞的原因:你可以發現一個函數是不可重入的,但很難確定一段復雜的代碼是可重入的
I believe they do, but then, evaluating a function is thread-safe or reentrant can be difficult. This is why I used the term smell above: You can find a function is not reentrant, but it could be difficult to be sure a complex piece of code is reentrant
假設您有一個對象,其中一個方法需要使用資源:
Let's say you have an object, with one method that needs to use a resource:
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
將在最后一次調用結束,并且可能在第一次調用結束之前使用.
The first problem is that if somehow this function is called recursively (i.e. this function calls itself, directly or indirectly), the code will probably crash, because this->p
will be deleted at the end of the last call, and still probably be used before the end of the first call.
因此,這段代碼不是遞歸安全的.
我們可以使用引用計數器來糾正這個:
We could use a reference counter to correct this:
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
的修改將使用 遞歸 互斥體(并非所有互斥體都是遞歸的)以原子方式完成:
This way, the code becomes recursive-safe… But it is still not reentrant because of multithreading issues: We must be sure the modifications of c
and of p
will be done atomically, using a recursive mutex (not all mutexes are recursive):
#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
的使用.
And of course, this all assumes the lots of code
is itself reentrant, including the use of p
.
上面的代碼甚至不是遠程異常安全,但這是另一個故事……^_^
And the code above is not even remotely exception-safe, but this is another story… ^_^
意大利面條式代碼確實如此.但是如果你正確地劃分你的代碼,你將避免重入問題.
It is quite true for spaghetti code. But if you partition correctly your code, you will avoid reentrancy problems.
他們必須只使用參數,他們自己的局部變量,其他沒有狀態的函數,如果他們有返回,則返回數據的副本.
They must only use the parameters, their own local variables, other functions without state, and return copies of the data if they return at all.
對象方法可以訪問this
,因此它與對象的同一實例的所有方法共享一個狀態.
An object method has access to this
, so it shares a state with all the methods of the same instance of the object.
因此,請確保該對象可以在堆棧中的某一點使用(即調用方法 A),然后在另一點(即調用方法 B)使用,而不會破壞整個對象.設計對象以確保在退出方法時,對象是穩定且正確的(沒有懸空指針、沒有相互矛盾的成員變量等).
So, make sure the object can be used at one point in the stack (i.e. calling method A), and then, at another point (i.e. calling method B), without corrupting the whole object. Design your object to make sure that upon exiting a method, the object is stable and correct (no dangling pointers, no contradicting member variables, etc.).
其他人不應訪問其內部數據:
No one else should have access to their internal data:
// 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 引用的代碼.
Even returning a const reference could be dangerous if the user retrieves the address of the data, as some other portion of the code could modify it without the code holding the const reference being told.
因此,用戶有責任使用互斥鎖來使用線程間共享的對象.
Thus, the user is responsible to use mutexes to use an object shared between threads.
來自 STL 的對象被設計為不是線程安全的(因為性能問題),因此,如果用戶想要在兩個線程之間共享一個 std::string
,用戶必須使用并發原語保護其訪問;
The objects from the STL are designed to be not thread-safe (because of performance issues), and thus, if a user want to share a std::string
between two threads, the user must protect its access with concurrency primitives;
這意味著如果您認為同一個資源可以被同一個線程使用兩次,請使用遞歸互斥鎖.
This means using recursive mutexes if you believe the same resource can be used twice by the same thread.
這篇關于什么是可重入函數?的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!