您的位置:首页 > Web前端

Reentrant vs Thread-safe

2016-06-29 11:40 288 查看


Reentrant vs Thread-safe

a whole copy of MagicJackTing's blog. thanks ot his sharing.

Reentrancy 和 thread-safty 是兩個容易被搞混了的觀念. 其中最嚴重的是誤以為 reentrant function 必定是 thread-safe 或者相反以為 thread-safe function 必為 reentrant, stackoverflow 網站上的答覆甚至同時出現二種答案的現象.


Reentrancy 和 Thread-safty 二者的差異

首先來看 reentrancy: 字面上的意思是可重入. Reentrancy 原先是討論單一執行緒環境下 (即沒有使用多工作業系統時) 的主程式和中斷服務程式 (ISR) 之間共用函數的問題. 主要的達成條件是共用函數不使用靜態變數或全域變數 (意即只用區域變數).

再來是 thread-safety: 字面上的意思是執行緒 (線程) 安全. Thread-safe 一開始就針對多執行緒的環境, 討論的是某一段程式碼在多執行緒環境中如何保持資料的一致性 (及完整性), 使不致於因為執行緒的切換而產生不一致 (及不完整) 或錯誤的結果. 問題的產生點一般出現在對某一共用變數 (或資源) 進行 read-modify-write 或者類似的動作時. 例如:

OK Example
Thread 1Thread 2Register

in CPU
 Variable

in RAM
    0
read 0<--0
inc by 1 1 0
write 1-->1
 read1<--1
 inc by 12 1
 write2-->2
NG Example
Thread 1Thread 2Register

in CPU
 Variable

in RAM
    0
read 0<--0
 read0<--0
inc by 1 1 0
 inc by 11 0
write 1-->1
 write1-->1
類似 read-modify-write 的動作還有 test-and-set, fetch-and-add, 和 compare-and-swap. 請參看 Wiki
Atomic Operation 相關說明

我們會把 reentrancy 和 thread-safety 搞混是因為它們的狀況近似, 問題都發生在一個函數 (或者是一小段程式碼) 執行時間重疊. 但是一個 thread-safe 函數不見得就一定是 reentrant. 舉例來說, 某個函數可以用一個 mutex 把原本的函數整個包裹起來 (如此可以避掉多執行緒環境引起的問題), 但是如果中斷服務程式 ISR 也使用到這個函數, 那它就可能在那兒苦苦的等不到 mutex 被先前鎖住它的執行緒把它釋放出來.

所以結論是 reentrancy 主要是檢討在 ISR 中函數庫裡的哪一些函數是可以呼叫的, 以及自己寫的函數可不可以在 ISR 中使用, 及如何撰寫才可以共用. 而 thread-safe 則是如何確保共用的資料/資源在多個執行緒之間 (不包含 ISR) 可以如預期的被使用.


實作上的要點

Reentrant 實作上比較簡單, 要注意:
不要使用共用資源 (global variables and static variable); 或者也可以在寫入共用變數之前, 把數值暫存在區域變數中, 使用完畢後回存.
不修改自身的程式碼
不呼叫 non-reentrant 函數. 例如: clib 中的 strtok(), rand(),srand() 都是
non-reentrant, 對應的 reentrant 版本是 strtok_r(),rand_r(), srand_r().

Thread-safe 實作的方法有很多, Wiki
網站提到 thread-safe 實作方法上可分為二類:
避免發生共用
使用可重入 (Reentrancy) 技術: 把靜態變數及全域變數全部改為區域變數 (區域變數通常放在 stack 區, 可以順利避免共用).
使用執行緒自身的儲存空間 (TLS, Thread Local Storage): 所以每一個執行緒都不同, 都有自己的一份拷貝. (C11 支援加上 keyword_Thread_local 來將變數移到
TLS; C++11 改用 keywordthread_local; gnu 或者其他 C++ compiler
則用 keyword__thread)

當無法避免共用時, 採用鎖定 (同步) 機制
使用互斥鎖 (Mutex, Mutual Exclusion): 利用序列化機制 (serialization) 來保證任一時間點都只有一個執行緒讀或寫共用資料. 但是多個 mutex 一起運作時需要小心仔細的對待, 不恰當的實作可能引起一些負作用, 如: deadlocks, livelocks, resource starvation.
使用原子操作 (AO, Atomic Operations): 在存取共用資料時禁止被其他執行緒打斷. 一般實作上需要一些新的硬體指令來支援, 它是實作執行緒鎖定的元件, 也是前一項 mutex 實作的基礎. 現代的多核 CPU (x86, MIPS, ARMv6 and later) 都至少有支援一對指令可以協助完成
AO 動作 (可能只支援部份 AO). 單核 CPU 在 OS 核心的部份可以用中斷鎖定 (disable/enable interrupt) 來支援 AO 的需求. 但是如果是在用戶空間 (user-space) 卻是不可以, 詳細請參考: Emulated
atomic operations and real-time scheduling.

使用不可變物件 (immutable objects): 物件構建 (construct) 之後即無法改變內容. 要實作改變時, 是以重新構建 (re-construct) 來取代修改現有之內容.


Part 2: C 語言例子 (reentrant function)

先來看 reentrant 的例子: 最常見到用來說明 reentrant 的例子大概就屬swap() 了

int t;

void swap(int *x,int *y) {
t = *x;
*x = *y;
*y = t;
}

void isr(void) {
int x = 1, y = 2;
swap(&x, &y);
...
return;
}

void main(void) {
int x = 3, y = 4;
...
swap(&x, &y);
...
}

這個例子中的 swap() 函數是 non-reentrant function
(而且也不是 thread-save). 原因是第4行及第6行用到共用的變數 t.
即便是我們用的是 32bit CPU, 甚至也有技援 mem. to mem. 移轉資料不被中斷, 也還是有二個時間點(在第5行執行前或執行後) 發生中斷會產生錯誤的執行結果. 如下表的 NG Example 1 和 NG Example 2 所示

OK Example
main()isr()*x*yt
swap();   -
t = *x; 343
*x = *y; 443
*y = t; 433
 swap();  3
 t = *x;121
 *x = *y;221
 *y = t;211
NG Example 1
main()isr()*x*yt
swap();   -
t = *x; 343
 swap();  3
 t = *x;121
 *x = *y;221
 *y = t;211
*x = *y; 441
*y = t; 411
NG Example 2
main()isr()*x*yt
swap();   -
t = *x; 343
*x = *y; 443
 swap();  3
 t = *x;121
 *x = *y;221
 *y = t;211
*y = t; 411
上面的程式只要稍微修改一下, 把第1行搬到第3,4行中間. 也就是把變數 t 變成區域變數, 即可將 swap() 變成
reentrant function.

void swap(int *x,int *y) {
int t;

t = *x;
*x = *y;
*y = t;
}

void isr(void) {
int x = 1, y = 2;
swap(&x, &y);
//...
return;
}

void main(void) {
intx = 3, y = 4;
//...
swap(&x, &y);
//...
}

Wiki 網站則刻意把共用的變數 t 保留著,
加入一個區域變數 s,
一樣可以使swap() 變成
reentrant function. (懷疑嗎? 可以仿照上面的例子畫一張表格填填看)

int t;

void swap(int *x,int *y) {
int s;

s = t;  // save global variable
t = *x;
*x = *y;
*y = t;
t = s;  // restore global variable
}

void isr(void) {
int x = 1, y = 2;
swap(&x, &y);
...
return;
}

void main(void) {
int x = 3, y = 4;
...
swap(&x, &y);
...
}


這樣就 OK 了嗎? 不...

這是一個刻意設計用來作解說而非常不實用的例子, 因為 main() 和 isr() 呼叫swap() 所用的參數都是自己
local 變數. 一但 main() 和 isr() 呼叫swap() 所用的參數其中有一個是二者共用的變數,
整個程式的行為就會很奇怪:
main() 和 isr() 呼叫 swap() 的次序不同結果不同
(邏輯上說不通)
swap() 又變成是
non-reentrant function 了 (試一下把變數 y 變成共用變數)

大家不可不慎.


Part 3: C 語言例子 (thread-safe function)

接下來, 我們來看一些 thread-safe 的例子: 首先是在 wiki
網站上的一個 Thread-safe 但不是 reentrant 的例子.

#include <pthread.h>

int increment_counter() {
static int counter = 0;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&mutex);

// only allow one thread to increment at a time
++counter;
// store value before any other threads increment it further
int result = counter;

pthread_mutex_unlock(&mutex);

return result;
}

這個例子中的 increment_counter() 可以被多個執行緒呼叫而不會產生任何問題, 因為它用了一個
mutex 來保護 (同步) 所有對共用的靜態變數 counter 的存取. 但是如果中斷服務程式
ISR 也呼叫了 increment_counter(), 就會很容易使系統當掉. 原因是如果中斷發生在執行緒正呼叫 increment_counter() 時
(尤其是 mutex lock 和 unlock 之間), 那 ISR 將永遠等不到 mutex 被 unlock. 因為 CPU 接受中斷進入 ISR 後, 只有 ISR 完成, 才會回到執行緒. 記住: 中斷永遠比正常執行優先, 所以 ISR 要比執行緒或者是 OS 核心優先執行.

接著下來的例子是從 http://www.thegeekstuff.com/2012/07/c-thread-safe-and-reentrant/ 節錄整理來的,
一樣是一個 Thread-safe 但不是 reentrant 的例子.

例子中意圖要控制一個字元陣列 arr 的元素依序被各個執行緒佔用.

//...
char arr[10];
int index = 0;

int func(charc) {

if(index >= sizeof(arr)) {
printf("\n No storage\n");
return -1;
}

arr[index] = c;
index++;

return index;
}
//...
很明顯的, 上面的函數 func() 一但被多個執行緒呼叫執行,
就破功了. 它的問題有二個:
第12行及第13行之間不可以被別的執行緒插斷, 原因是 index 還沒來得及
+1 以保護剛存入陣列元素的字元變數 c .
第7行取出共用變數 index 來檢查,
但是萬一在第12~13行還沒執行前就被別的執行緒插斷, 等到回復執行之後變數 index 值很可能已經被更動了,
但是 CPU 暫存器中的拷貝卻沒有更新, 而接著執行的第12~13行就會覆蓋了已經被別的執行緒所佔用的陣列元素了.

所以上面的例子必需適當的修改 (這裡只用註解標記應該加入 mutex lock/unlock 的修改處), 如:

//...
char arr[10];
int index = 0;

int func(charc) {
int tmp;

/* Lock a mutex here */
tmp = index;

if(index >= sizeof(arr)) {
/* unlock the mutex here */
printf("\n No storage\n");
return -1;
}
index++;
/* unlock the mutex here */

arr[tmp] = c;
return tmp;
}
//...
上例的修改可能比較不那麼好 (好看及好維護), 原因是 mutex lock/unlock 不對稱, 出現了 2 個 mutex unlock. 但它還有一個重點是: 先佔用資源 (index++),
再把值存入. 這麼作可以早一點點把 mutex 放開 (重要!). 改寫成下面這個樣子可能看起來好些 (一樣只用註解標記應該加入 mutex lock/unlock 的修改處).

//...
char arr[10];
int index = 0;

int func(charc) {
int tmp = -1;

/* Lock a mutex here */
if(index < sizeof(arr))
tmp = index++;
/* unlock the mutex here */

if(tmp < 0)
printf("\n No storage\n");
else
arr[tmp] = c;

return tmp;
}
//...
修改過後的 func() 可以被多個執行緒呼叫執行而不會發生存放位置位置索引不一致的問題了. 請注意: 用了
mutex 就不是 reentrant function.

Reference:

http://magicjackting.pixnet.net/blog/post/113860339
http://magicjackting.pixnet.net/blog/post/113859925 http://magicjackting.pixnet.net/blog/post/117448003
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  reentrancy thread