Reentrant vs Thread-safe

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 1 Thread 2 Register
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 1 Thread 2 Register
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 實作上比較簡單, 要注意:

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

Thread-safe 實作的方法有很多, Wiki 網站提到 thread-safe 實作方法上可分為二類:

  1. 避免發生共用
    • 使用可重入 (Reentrancy) 技術: 把靜態變數及全域變數全部改為區域變數 (區域變數通常放在 stack 區, 可以順利避免共用).
    • 使用執行緒自身的儲存空間 (TLS, Thread Local Storage): 所以每一個執行緒都不同, 都有自己的一份拷貝. (C11 支援加上 keyword_Thread_local 來將變數移到 TLS; C++11 改用 keywordthread_local; gnu 或者其他 C++ compiler 則用 keyword__thread)
  2. 當無法避免共用時, 採用鎖定 (同步) 機制
    • 使用互斥鎖 (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 *y t
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 *y t
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 *y t
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()  一但被多個執行緒呼叫執行, 就破功了. 它的問題有二個:

  1. 第12行及第13行之間不可以被別的執行緒插斷, 原因是 index 還沒來得及 +1 以保護剛存入陣列元素的字元變數 c .
  2. 第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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值