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
  read 1 <-- 1
  inc by 1 2   1
  write 2 --> 2
NG Example
Thread 1 Thread 2 Register
in CPU
  Variable
in RAM
        0
read   0 <-- 0
  read 0 <-- 0
inc by 1   1   0
  inc by 1 1   0
write   1 --> 1
  write 1 --> 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;   3 4 3
*x = *y;   4 4 3
*y = t;   4 3 3
  swap();     3
  t = *x; 1 2 1
  *x = *y; 2 2 1
  *y = t; 2 1 1
NG Example 1
main() isr() *x *y t
swap();       -
t = *x;   3 4 3
  swap();     3
  t = *x; 1 2 1
  *x = *y; 2 2 1
  *y = t; 2 1 1
*x = *y;   4 4 1
*y = t;   4 1 1
NG Example 2
main() isr() *x *y t
swap();       -
t = *x;   3 4 3
*x = *y;   4 4 3
  swap();     3
  t = *x; 1 2 1
  *x = *y; 2 2 1
  *y = t; 2 1 1
*y = t;   4 1 1

上面的程式只要稍微修改一下, 把第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 variablet = *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 furtherint 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");elsearr[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

Reentrant vs Thread-safe相关推荐

  1. mysql thread safe_Windows环境下完全手工配置Apache、MySQL和PHP(Thread Safe)

    happydagui:现在LAMP(Linux.Apache.MySQL.PHP/Perl/Python的简称)已经很流行了.在Windows下也有类似的,比如 WAMP(Apache, MySQL, ...

  2. PHP5 VC9、VC6、Thread Safe、Non Thread Safe各个版本区别

    2019独角兽企业重金招聘Python工程师标准>>> 一.如何选择PHP5.3的VC9版本和VC6版本 网站推广 VC6版本是使用Visual Studio 6编译器编译的,如果你 ...

  3. PHP版本VC6与VC9/VC11/VC14、Thread Safe与None-Thread Safe等的区别

    原文:PHP版本VC6与VC9/VC11/VC14.Thread Safe与None-Thread Safe等的区别 最近正好在弄一个PHP的程序,在这之前一直没有怎么以接触,发现对PHP版本知识了解 ...

  4. php5.6non thread safe 区别,PHP版本Non Thread Safe和Thread Safe如何选择?区别是什么?

    PHP版本分为Non Thread Safe和Thread Safe,Non Thread Safe是指非线程安全,Thread Safe是指线程安全,区别是什么?如何选择? Non Thread S ...

  5. PHP关于VC11,VC9,VC6以及Thread Safe和Non Thread Safe版本选择

    2019独角兽企业重金招聘Python工程师标准>>> 这里是我在搭建php环境时收集的资料供大家参考: 现在PHP官网上下载PHP安装包都有VC11或VC9的字样,这是什么含义,我 ...

  6. PHP 5.3 下载时 VC9、VC6、Thread Safe、Non Thread Safe 是什么意思?

    我最近在 PHP 官网上看到又有新版的 PHP 下载了,于是上去找找 For Windows 的版本,可是一看确傻眼了,一共给了四个版本,VC9 x86 Non Thread Safe.VC9 x86 ...

  7. PHP版本选择讲解:VC6与VC9,Thread Safe与None-Thread Safe等的选择

    October 28, 2010 | 作者:白菜 最近发现很多PHP程序员对PHP版本知识了解不是很清楚,自己也看了不少类似的文章,还是感觉不够明确和全面,网上的结论又都是模棱两可,在此,给出最完整甚 ...

  8. non thread safe php vc11,PHP 中什么线程安全(TS)和非线程安全(NTS)

    显示行号 | 选择喜欢的代码风格 默认 GitHub Dune LakeSide Plateau Vibrant Blue Eighties Tranquil Windows 版的 PHP 从版本 P ...

  9. 关于VC9和VC6以及Thread Safe和Non Thread Safe版本选择的问题

    一.如何选择PHP5.3的VC9版本和VC6版本 VC6版本是使用Visual Studio 6编译器编译的,如果你的PHP是用Apache来架设的,那你就选择VC6版本. VC9版本是使用Visua ...

  10. PHP版本VC6与VC9、Thread Safe与None-Thread Safe等的区别

    转载:http://www.cnblogs.com/whoknows/articles/2425841.html 最近发现很多PHP程序员对PHP版本知识了解不是很清楚,自己也看了不少类似的文章,还是 ...

最新文章

  1. mysql删除表中的唯一索引吗_Mysql 使用sql删除同表中重复数据并加唯一索引
  2. 7000 界面语言不升级_仅限今日!手把手教你C++图形界面开发|附完整代码,海量干货!...
  3. 手机画面尺寸多少满屏_手机屏幕科普
  4. linux下使用yum安装新版php7.0
  5. 微软推出免费在线系统诊断工具--不用手动下载
  6. DataGrid中加入CheckBox,并实现单选 选择自 listhome 的 Blog
  7. Oracle 角色权限表
  8. QQ机器人实现RSS订阅(github项目)
  9. dirent.h缺失,Microsoft Visual Studio 2019( Professional)解决方案
  10. 两级缓存框架J2Cache的使用
  11. 修改xshell的配色方案
  12. 精确字符串匹配(Zbox算法)
  13. 对于硬盘做了raid的Windows server 2016服务器重置密码
  14. CheckForIllegalCrossThreadCalls = false
  15. python 地址簿
  16. arcgis 栅格计算器,img叠加运算,con函数,img转txt
  17. 关于路由器当无线交换机用
  18. 郑州大学微型计算机原理与接口技术,《微机原理及接口技术》第01章在线测试...
  19. 使用GAppProxy时安全证书无效的解决办法
  20. JAVA银企直连建设银行云直连模式超详细讲解

热门文章

  1. excel快速填充_Excel高手最常用的21个Ctrl 键
  2. struct ifreq 学习,现实ifconfig 功能
  3. 连吵架都是队列和栈,学计算机的女生,是一种怎样的存在?
  4. utf-8 python 乱码prinnt u_为什么用gb2312解码内涵段子吧得到的中文是乱码?
  5. C4D操作 延迟 ,卡顿罕见状况解决方案。
  6. Linux修改内核参数
  7. 小白如何使用DSW玩转天池NLP算法大赛
  8. ajax-210810-03---练习全局刷新计算bmi
  9. python手写数字识别教学_6手写数字识别_python机器学习与数据挖掘_Python视频-51CTO学院...
  10. 学无止境,打算停更一段时间