2017年9月1日 星期五

function reentrant, thread-safe, memory ordering (memory barrier)

the 1st edition: 20130301
the 2nd edition: 20151001
the 3rd edition: 20170727
the 4th edition: 20180427
這篇是個雜記, 紀錄這三個惱人的東西還有一些自己的問題。

http://tw.myblog.yahoo.com/blue-comic/article?mid=253&prev=257&l=f&fid=26
http://blog.xuite.net/jackie.xie/bluelove/46644355

在寫怎麼樣的程式時, 需要考慮 reentrant?
在寫怎麼樣的程式時, 需要考慮 thread-safe?

在 multi-thread 程式中, 需要的是 reentrant 還是 thread-safe?

問題很混亂, 不過看了 pthread 多緒程式設計 (p237), 有了比較清楚的概念:

簡單提一下 (詳細請參閱該書籍):

thread safe: 使用 mutex 來保護 global data, 而 data 不一定是變數, 可能是檔案, 可能是某個週邊。由於用了 lock, 很有可能會有 dead lock 的問題。

reentrant function: 不可使用 static, global data, 一定可以正常執行, 也不會有 dead lock 的問題。

那 reentrant function 裡頭用 mutex 保護共用資源算 thread safe 嗎? 算, 不過那就不是 reentrant function 了, 請參考 malloc() is non-reentrant but thread-safe? [duplicate] 提到 reentrant, thread-safe 是如何的不同?

malloc 是 thread safe, 但不是 reentrant, 該怎麼解釋呢? reentrant 和 thread 的行為有點微妙的不同 (感謝 tg 的朋友解惑):

list 1. thread
1 thread:
2 void thread_1()
3 {
4   malloc()
5   {
6      lock();
7 
8      set_list();
9 
10     unlock();
11   }
12 }
 
13 void thread_2()
14 {
15   malloc()
16   {
17     lock();
18 
19     set_list();
20 
21    unlock();
    }
   }

list 1 thread_1() 執行到 set_list() 時, 換 thread_2 執行, list 1 L17 會卡住 thread_2, 等到 scheduler 將執行權切回 list 1 L8 後, 執行 L10 的 unlock 後, 若這時候切回 thread_2, thread_2 得以繼續執行。

reentrant 會和中斷扯上關係:

list 2. reentrant
01 void func_1()
02 {
03   malloc()
04   {
05     lock();
06 
07     set_list();
08 
09     unlock();
10   }
11 }
 
12 void isr_1()
13 {
14   malloc()
15   {
16     lock();
17 
18     set_list();
19 
20     unlock();
     }
   }

list 2 func_1 執行到 set_list() 時, 中斷發動了, 改為執行 isr_1(), isr_1 卡在 list 2 L16, 但程式再也回不到 list 2 L7 那行了, 在中斷 isr 中, 沒有做完是切不回去 func_1 的, 這是和 thread 最不一樣的行為, 所以會一直卡在 list 2 L16 那行。

所以才說, malloc 是 thread safe, 但不是 reentrant。

Reentrancy and Thread-Safety
Hence, a thread-safe function is always reentrant, but a reentrant function is not always thread-safe.

Linux环境编程:从应用到内核 (Linux/Unix技术丛书) 0.4.5 可重入函数

可重入函数一定是线程安全的, 而线程安全函数则不一定是可重入函数。

以上兩個說法完全相反, 該聽誰的呢?

我自己是贊同《Linux 环境编程》的說法。但不用太拘泥名詞解釋, 了解本質上的不同是比較重要的。

reentrant.cpp
 1 #include <signal.h>
 2 #include <pthread.h>
 3 
 4 #include <chrono>
 5 #include <iostream>
 6 using namespace std;
 7 using namespace std::chrono;
 8 
 9 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
10 
11 volatile int i=5;
12 
13 void thread_safe_func()
14 {
15   pthread_mutex_lock(&mutex);
16 
17   for (int i=0 ; i < 200000 ; ++i)
18     for (int i=0 ; i < 10000 ; ++i)
19     {
20     }
21   i = 1;
22 
23   pthread_mutex_unlock(&mutex);
24 }
25 
26 void sig_handle(int sig)
27 {
28   thread_safe_func();
29 }
30 
31 int main(int argc, char *argv[])
32 {
33   signal(SIGINT, sig_handle);
34   thread_safe_func();
35   #if 0
36   time_point<steady_clock> tp1 = steady_clock::now();
37 
38 
39   time_point<steady_clock> tp2 = steady_clock::now();
40 
41   typedef duration<unsigned long long, ratio<1,1000000>> unit;
42   unit d = duration_cast<unit>(tp2 - tp1);
43   cout << d.count() << endl;
44   #endif
45   return 0;
46 }

reentrant.cpp 是我模仿 malloc 而來, 在程式執行後, 按下 ctrl+c SIGINT, 這時候用 ps 觀察

ps aux |grep reentrant
descent   7210  0.2  0.0  15232  1640 pts/68   S+   10:12   0:01 ./reentrant

PROCESS STATE CODES
  Here are the different values that the s, stat and state output specifiers 
  (header "STAT" or "S") will display to describe the state of a process:

          D    uninterruptible sleep (usually IO)
          R    running or runnable (on run queue)
          S    interruptible sleep (waiting for an event to complete)
          T    stopped, either by a job control signal or because it is being traced
          W    paging (not valid since the 2.6.xx kernel)
          X    dead (should never be seen)
          Z    defunct ("zombie") process, terminated but not reaped by its parent

  For BSD formats and when the stat keyword is used, additional characters may be displayed:

          <    high-priority (not nice to other users)
          N    low-priority (nice to other users)
          L    has pages locked into memory (for real-time and custom IO)
          s    is a session leader
          l    is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
          +    is in the foreground process group

處在 interruptible sleep (waiting for an event to complete), 程式無法正常結束, 應該是卡在 L15。

reentrant.cpp L17, L18 的 for loop 單純用來 delay, 在我系統上, 大概是 3 秒, 為的是讓 SIGINT 有時間可以在

15   pthread_mutex_lock(&mutex);
            ...
23   pthread_mutex_unlock(&mutex);

這中間的時候送出。

而程式也無法正常結束。

ref What does the “interruptible sleep” state indicate?


ref:



再來是 memory ordering (memory barrier) 這問題:

我也被這問題困惑很久, 在 os 下寫應用程式, 幾乎沒用過這種指令, 有需要在 os 下的應用程式使用這些指令嗎? 還是被 os api 給包裝起來了呢?

這是 linux 在 x86 下使用的 memory barrier 指令。
#define mb() asm volatile("mfence":::"memory")
#define rmb() asm volatile("lfence":::"memory")
#define wmb() asm volatile("sfence" ::: "memory")

arm cortex m3 memory barrier 指令: isb, dsb

Binary Hacks - 駭客秘傳技巧一百招 #94 在談這個, 看完就清楚了, 可是我不知道怎麼寫程式測試這個情形。

不過這樣的話, 在 os 下寫應用程式也會遇到這問題才是, 可是卻沒看過在 os 下的 c 應用程式用上這種指令。

multi-thread 程式要突破兩關:
  1. lock 機制 (使用 test-and-set 硬體指令實作)
  2. memory ordering (使用 memory barrier 指令)
才有正確的結果。

1 通常由 os api 提供 (x86 可用 bts/xchg 實作, arm cortex M3 可用 ldrex/strex 來實作)。
2 我就沒看過在 os 下的應用程式要怎麼用, 只看過在 driver 階段有使用過。這篇提到 memory barrier pthread 已經實作在裡頭了。

thread 程式的共享變數還需要用 volatile 來宣告, 僅僅加上 lock 是不夠的。
ref:
详解C语言中volatile关键字
剖析为什么在多核多线程程序中要慎用volatile关键字?

ref memory barrier (ptt damody provides):

下一個類似的問題是:
mutex/semaphore and spin lock, 你知道的, 我要的不是教科書的解釋, 我想要實作這些東西出來, 能實作出來就真能代表懂了。

目前我在 arm cortex-m3 使用 ldrex/strex 實作出 spin lock。

Linux内核同步机制之(一):原子操作
這篇做了很好的解釋。

Linux内核同步机制之(三):memory barrier

蜗窝科技出版的文章, 質/量都很好。

ref:
Fast Atomic Counters With the x86 LOCK Prefix:
http://www.codemaestro.com/reviews/8

1 則留言:

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。