可重入性 線程安全 Async-Signal-Safe

Shou哥說遇到問題才能學到東西。

還有半句沒說:前提是靜心鑽研下去,而不是煩躁並避開問題

程序出了問題,用文件作爲一個mutex使用共享內存,寫了數據就發信號,結果收到信號Qt寫的GUI會卡死,看sigqueue的manpage,發現Async-signal-safe,搜索到這篇


    首先,可重入和線程安全是兩個並不等同的概念,一個函數可以是可重入的,也可以是線程安全的,可以兩者均滿足,可以兩者皆不滿組(該描述嚴格的說存在漏洞,參見第二條)。

    其次,從集合和邏輯的角度看,可重入是線程安全的子集,可重入是線程安全的充分非必要條件。可重入的函數一定是線程安全的,然過來則不成立。

    第三,POSIX 中對可重入和線程安全這兩個概念的定義:
   
    Reentrant Function :

    A function whose effect, when called by two or more threads,is guaranteed to be as if the threads each executed thefunction one after another in an undefined order, even ifthe actual execution is interleaved.

                                                                                                        From IEEE Std 1003.1-2001 (POSIX 1003.1)
                                                                                                                                      -- Base Definitions, Issue 6
    Thread-Safe Function
     
    A function that may be safely invoked concurrently by multiple threads.

   另外還有一個 Async-Signal-Safe的概念

    Async-Signal-Safe Function:
     
    A function that may be invoked, without restriction fromsignal-catching functions. No function is async-signal -safe unless explicitly described as such.

    以上三者的關係爲:
   
    Reentrant Function 必然是Thread-Safe FunctionAsync-Signal-Safe Function
 
  
可 重入與線程安全的區別體現在能否在signal處理函數中被調用的問題上,可重入函數在signal處理函數中可以被安全調用,因此同時也是Async- Signal-Safe Function;而線程安全函數不保證可以在signal處理函數中被安全調用,如果通過設置信號阻塞集合等方法保證一個非可重入函數不被信號中斷,那 麼它也是Async-Signal-Safe Function。

     值得一提的是POSIX 1003.1的System Interface缺省是Thread-Safe的,但不是Async-Signal-Safe的。Async-Signal-Safe的需要明確表示,比如fork ()和signal()。

最後讓我們來構想一個線程安全但不可重入的函數:

   假設函數func()在執行過程中需要訪問某個共享資源,因此爲了實現線程安全,在使用該資源前加鎖,在不需要資源解鎖。

   假設該函數在某次執行過程中,在已經獲得資源鎖之後,有異步信號發生,程序的執行流轉交給對應的信號處理函數;再假設在該信號處理函數中也需要調用函數 func(),那麼func()在這次執行中仍會在訪問共享資源前試圖獲得資源鎖,然而我們知道前一個func()實例已然獲得該鎖,因此信號處理函數阻 塞——另一方面,信號處理函數結束前被信號中斷的線程是無法恢復執行的,當然也沒有釋放資源的機會,這樣就出現了線程和信號處理函數之間的死鎖局面。

    因此,func()儘管通過加鎖的方式能保證線程安全,但是由於函數體對共享資源的訪問,因此是非可重入。


評論

# manio  發表於2008-08-02 22:08:28  IP: 10.62.79.*
“可重入是線程安全的充分非必要條件”

class Counter
{
public:
Counter() { n = 0; }

void increment() { ++n; }
void decrement() { --n; }
int value() const { return n; }

private:
int n;
};
這就是一個可重入但線程不安全的例子。

Most Qt classes are reentrant and not thread-safe, to avoid the overhead of repeatedly locking and unlocking a QMutex. For example, QString is reentrant, meaning that you can use it in different threads, but you can't access the same QString object from different threads simultaneously (unless you protect it with a mutex yourself). A few classes and functions are thread-safe; these are mainly thread-related classes such as QMutex, or fundamental functions such as QCoreApplication::postEvent().

=================================================

先上定義吧,POSIX對它們的定義 分別是:

Reentrant Function

A function whose effect, when called by two or more threads, is guaranteed to be as if the threads each executed the function one after another in an undefined order, even if the actual execution is interleaved.

Thread-Safe

A function that may be safely invoked concurrently by multiple threads. Each function defined in the System Interfaces volume of IEEE Std 1003.1-2001 is thread-safe unless explicitly stated otherwise.

Async-Signal-Safe Function

A function that may be invoked, without restriction, from signal-catching functions. No function is async-signal-safe unless explicitly described as such.

可重入我們都清楚,顧名思義,就是可以重新進入,進一步講就是,用相同的輸入,每次調用函數一定會返回相同的結果。這就是可重入。wikipedia上 有更嚴謹的定義:

* Must hold no static (global) non-constant data.
* Must not return the address to static (global) non-constant data.
* Must work only on the data provided to it by the caller.
* Must not rely on locks to singleton resources.
* Must not call non-reentrant computer programs or routines.

然後是線程安全 ,從定義上看,它僅要求了可以安全地被線程併發執行。這是一個相對較低的要求,因爲它內部可以訪問全局變量或靜態變量,不過需要加鎖,也就是說,只要是在線程可控之中的,每次調用它返回不同的結果也沒關係。到這裏我們可以看出:可重入函數一定是線程安全的,而反之未必。 wikipedia上也寫道:

Therefore, reentrancy is a more fundamental property than thread-safety and by definition, leads to thread-safety: Every reentrant function is thread-safe, however, not every thread-safe function is reentrant.

例子,有很多,最出名的莫過於strtok(3),我們認識可重入這個概念就是從它開始的,它內部適用了靜態變量,顯然是不可重入的(它的可重入版是strtok_r(3))。其次應該是malloc(3),嘿嘿,其實也很明顯,我就不多說了。但是,strtok(3)不是 線程安全的,而malloc(3)是。

還有一個概念我們不常碰到,那就是異步信號安全,它其實也很簡單,就是一個函數可以在信號處理函數中被安全地調用。看起來它似乎和線程安全類似,其 實不然,我們知道信號是異步產生的,但是,信號處理函數是打斷主函數(相對於信號處理函數)後執行,執行完後又返回主函數中去的。也就是說,它不是 併發的!

一個函數,它訪問了全局變量,那麼它就是不可重入的,不過我們可以把它變成線程安全的,加上鎖就可以,但是這種方法並不會把它變成異步信號安全的, 而幾乎可以肯定的是,使用了鎖的一定不是信號安全的(除非屏蔽了信號,顯然),信號完全可以在鎖加上後解開前到來,然後就很可能形成死鎖…… 這裏 有個很好的例子。所以,可重入的函數也一定是異步信號安全的,而反之未必。可以參考IBM上一篇不錯的文章

關於異步信號安全的函數列表可以參考man 7 signal ;關於 線程安全的函數列表可以參考APUE第12.5節 ;關於可重入函數列表,可參考APUE第10.6節 。另請參閱

 

==========================================================================

信號 signal handler是如何造成死鎖的, 寫得很清楚

Signals are delivered asynchronously, much like interrupts, and so there are a great deal of restrictions placed on the code which runs. Many of these restrictions are much like ThreadSafety , in that you have to account for the fact that your signal handler could run at any moment while other code is running, causing weird problems.

Here is an example of code which is safe:

 

volatile int x = 0;

void handler(int signal) {
x++;
}

int main(void) {
signal(SIGHUP, handler);
while(1) {
sleep(1);
printf("x = %d/n", x);
}
}

Just like with threads, if the signal handler is writing to a primitive and the main function is only reading, everything is safe. Well, as long as no one else sends a SIGHUP: if that were to happen, then the above example has the potential of a race condition.

Now for a broken example:

 

volatile int x = 0;

void handler(int signal) {
x++;
}

int main(void) {
signal(SIGHUP, handler);
while(1) {
sleep(1);
x++;
printf("x = %d/n", x);
}
}

This is broken for the same reason that it would be broken with two threads. The operation x++ is not necessarily atomic, but can be divided into several pieces:

 

read x from memory into a register
increment the register
write x back into memory

If the signal handler is invoked while the main function is in the middle of this update process, an increment will be lost. Worse, the read and write of x is not guaranteed to be atomic (for that we would need to use sig_atomic_t ). Let's try to fix this by adding a simple lock. We'll assume that we have a TestAndSet? function which atomically sets a variable to 1 and returns its old value. Then we write our new program like this:

 

volatile sig_atomic_t lock = 0;
volatile int x = 0;

void handler(int signal) {
while(TestAndSet(&lock)) ;
x++;
lock = 0;
}

int main(void) {
signal(SIGHUP, handler);
while(1) {
sleep(1);

while(TestAndSet(&lock)) ;
x++;
lock = 0;

printf("x = %d/n", x);
}
}

If we were working with threads, then everything would work as expected. But with signals, this not only fails to solve the problem, but it actually makes it worse . Why?

Threads run more or less simultaneously. On a multi-processor system they might really run simultaneously, but even on a single-processor system, the OS makes sure that every thread gets a chance to run. So while a thread might get stuck in the while loop for a while, eventually the other thread will get a chance to run, and it will unlock the lock.

Signals, however, don't run simultaneously. While the signal handler is running, the main program is completely stopped. If the handler is invoked while the main program has locked the lock, the handler will spin forever waiting for the lock to be unlocked, while at the same time the main program is stuck waiting for the handler to end. Deadlock! If this situation ever happens, your program will completely freeze. So now we see that signal-safe code is even more restricted than thread-safe code.

How do we fix it? For our example program, we can fix it by adding an auxiliary variable, like so:

 

volatile sig_atomic_t lock = 0;
volatile int x = 0;
volatile int y = 0;

void handler(int signal) {
if(!lock)
x++;
else
y++;
}

int main(void) {
int temp;
signal(SIGHUP, handler);
while(1) {
sleep(1);

lock = 1
x++;
lock = 0;

temp = y;
y = 0;

lock = 1;
x += temp;
lock = 0;

printf("x = %d/n", x);
}
}

Here, we have a sort of asymmetric lock. Instead of waiting for the lock to be free, the signal handler uses it to decide which variable to increment. The main program then manipulates the lock to ensure that it can always reliably read or modify the variable it's interested in while it performs the sum of the two.

But you say, this counter is nonsense. I just want to print out a notice that my signal was received, nothing more. I'll just write this code:

 

void handler(int signal) {
printf("got signal %d/n", signal);
}

This code is fine, right? None of this nonsense with locks or counters or anything. No! It's not safe because you're calling printf() , and who knows what it does inside. In fact printf() probably does some locking internally on the file stream, so if the signal is delivered while your program is in the middle of another call to printf() , kaboom, deadlock!

What do we do? In this case, we'll have to do everything manually using functions which we know to be safe. In this case, we take advantage of the fact that write() is safe:

 

void handler(int signal) {
char text[] = "got signal 00/n";
text[11] += (signal / 10) % 10;
text[12] += signal % 10;
write(STDOUT_FILENO, text, 14);
}

The key word is "async-signal safe". If you see a function documented as being "thread safe", you know that you can call it simultaneously from multiple threads. If you see a function documented as being "async-signal safe", then you know that you can call it from a signal handler without blowing up your program. A fairly complete list of signal safe functions can be found in man sigaction .

The trick is that a lot of code is not async-signal safe. Since it's so much harder to write, very little code is async-signal safe. For example, objc_msgSend() uses locks and so is not async-signal safe, meaning that you cannot use any Objective-C code in a signal handler. You can't use Objective-C, you can't call malloc() , you can't touch CoreFoundation or most of libc.

How do you get anything accomplished in a signal handler, then? The best bet is usually to do as little as possible in the handler itself, but somehow signal the rest of your program that something needs to be done. For example, let's say you want to reload your configuration file when sent a SIGHUP. If your program never blocks for long, we could write our program like this:

 

volatile sig_atomic_t gReloadConfigFile = 0;

void handler(int signal) {
gReloadConfigFile = 1;
}
...
while(!done) {
DoPeriodicProcessing();
if(gReloadConfigFile) {
gReloadConfigFile = 0;
ReloadConfigFile();
}
}

What if your program often blocks on input or a socket or something of that nature? All is not lost, however, because delivering a signal will automatically unblock your program if it's in the middle of a blocking read() , select() , or similar. You could write your program like this:

 

volatile sig_atomic_t gReloadConfigFile = 0;

void handler(int signal) {
gReloadConfigFile = 1;
}
...
while(!done) {
if(gReloadConfigFile) {
gReloadConfigFile = 0;
ReloadConfigFile();
}
// X
select(...);
ProcessData(...);
}

Looks good, right? If your program is blocked in the select() , the signal will dislodge it and the app will reload its configuration file. If the program is busy processing data when it comes in, then the configuration file will be reloaded before you get back to the select() . But... you guessed it! This code isn't completely safe!

The key is the // X comment. If the signal is delivered in that spot, after the check but before entering the select() , the select() will still block, and the configuration file won't be reloaded until some input arrives, which could be much later.

What do we do about this? The best way is to use a signaling mechanism which can be integrated into the select() call, namely a pipe. You can create a pipe using the pipe() system call, then read from it in the select() and write to it in your signal handler. By using the pipe instead of a global variable, you ensure that the select() never blocks when a condition needs to be taken care of. Implementation of this scheme is left as an exercise to the reader.

If you do use this scheme, you have to be careful. (I can hear you saying, "What now???") Make sure to set your pipe to be non-blocking, otherwise your pipe could fill up in the signal handler before its emptied in your main program, and you'll deadlock. If the write fails because the pipe is full then that's fine anyway, because that means there's already a signal sitting in the pipe ready to be received the next time you go through your app's main loop.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章