reentrant函数与thread safe函数浅析
时间:2008-01-16 11:13:49 来源: 作者:
|
记得以前讨论过一个关于reentrant函数与thread safe函数的帖子 很多人对于这两种函数不是很了解, 尤其是发现malloc等函数是non-reentrant函数时,对多线程编程都产生了"恐惧" 这里是我对这两种函数的一些理解,希望和大家探讨一些.欢迎批评指正. 1. reentrant函数 一个函数是reentrant的,如果它可以被安全地递归或并行调用。要想成为reentrant式的函数,该函数不能含有(或使用)静态(或全局)数据(来存储函数调用过程中的状态信息),也不能返回指向静态数据的指针,它只能使用由调用者提供的数据,当然也不能调用non-reentrant函数. 比较典型的non-reentrant函数有getpwnam, strtok, malloc等. reentrant和non-reentrant函数的例子
试验方法: 1. 编译 gcc test.c -lpthread 在一个终端中运行 ./a.out, 在另一个终端中运行 ps -A|grep a.out可以看到该进程的id 2. 用如下方式运行a.out: 运行./a.out,在按回车前,在另外一个终端中运行kill -14 pid (这里的pid是运行上面的ps时看到的值) 然后,按回车继续运行a.out就会看到2^5 = 8 的错误结论 对于函数int* getPower(int i) 由于函数getPower会返回一个指向静态数据的指针,在第一次调用getPower的过程中,再次调用getPower,则两次返回的指针都指向同一块内存,第二次的结果将第一次的覆盖了(很多non-reentrant函数的这种用法会导致不确定的后果).所以是non-reentrant的. 对于函数void getPower_r(int i, int* result) getPower_r会将所得的信息存储到result所指的内存中,它只是使用了由调用者提供的数据,所以是reentrant.在信号处理函数中可以正常的使用它. 2. thread-safe函数 Thread safety是多线程编程中的概念,thread safe函数是指那些能够被多个线程同时并发地正确执行的函数. thread safe和non thread safe的例子
函数func是non thread safe的,这是因为它不能避免对共享数据count的race condition, 设想这种情况:一开始count是0,当线程1进入func函数,判断过count == 0后,线程2进入func函数 线程2判断count==0,并执行count++,然后线程1开始执行,此时count != 0 了,但是线程1仍然要执行 count++,这就产生了错误. func_s通过mutex锁将对共享数据的访问锁定,从而避免了上述情况的发生.func_s是thread safe的 只要通过适当的"锁"机制,thread safe函数还是比较好实现的. 3. reentrant函数与thread safe函数的区别 reentrant函数与是不是多线程无关,如果是reentrant函数,那么要求即使是同一个进程(或线程)同时多次进入该函数时,该函数仍能够正确的运作. 该要求还蕴含着,如果是在多线程环境中,不同的两个线程同时进入该函数时,该函数也能够正确的运作. thread safe函数是与多线程有关的,它只是要求不同的两个线程同时对该函数的调用在逻辑上是正确的. 从上面的说明可以看出,reentrant的要求比thread safe的要求更加严格.reentrant的函数必是thread safe的,而thread safe的函数 未必是reentrant的. 举例说明:
试验方法: 1. 编译 gcc test.c -lpthread 在一个终端中运行 ./a.out, 在另一个终端中运行 ps -A|grep a.out可以看到该进程的id 2. 进行下面4次运行a.out: 每次运行分别在第1,2,3,4次回车前,在另外一个终端中运行kill -14 pid (这里的pid是上面ps中看到的值) 试验结果: 1. 该进程中有3个线程:一个主线程,两个子线程 2. func_s是thread safe的 3. func_s不是reentrant的 4. 信号处理程序会中断主线程的执行,不会中断子线程的执行 5. 在第1,4次回车前,在另外一个终端中运行kill -14 pid会形成死锁,这是因为 主线程先锁住了临界区,主线程被中断后,执行handler(以主线程执行),handler试图锁定临界区时, 由于同一个线程锁定两次,所以形成死锁 6. 在第2,3次回车前,在另外一个终端中运行kill -14 pid不会形成死锁,这是因为一个子线程先锁住 了临界区,主线程被中断后,执行handler(以主线程执行),handler试图锁定临界区时,被挂起,这时,子线程 可以被继续执行.当该子线程释放掉锁以后,handler和另外一个子线程可以竞争进入临界区,然后继续执行. 所以不会形成死锁. 结论: 1. reentrant是对函数相当严格的要求,绝大部分函数都不是reentrant的(APUE上有一个reentrant函数 的列表). 什么时候我们需要reentrant函数呢?只有一个函数需要在同一个线程中需要进入两次以上,我们才需要 reentrant函数.这些情况主要是异步信号处理,递归函数等等.(non-reentrant的递归函数也不一定会 出错,出不出错取决于你怎么定义和使用该函数). 大部分时候,我们并不需要函数是reentrant的. 2. 在多线程环境当中,只要求多个线程可以同时调用一个函数时,该函数只要是thread safe的就可以了. 我们常见的大部分函数都是thread safe的,不确定的话请查阅相关文档. 3. reentrant和thread safe的本质的区别就在于,reentrant函数要求即使在同一个线程中任意地进入两次以上, 也能正确执行. 大家常用的malloc函数是一个典型的non-reentrant但是是thread safe函数,这就说明,我们可以方便的 在多个线程中同时调用malloc,但是,如果将malloc函数放入信号处理函数中去,这是一件很危险的事情. 4. reentrant函数肯定是thread safe函数,也就是说,non thread safe肯定是non-reentrant函数 不能简单的通过加锁,来使得non-reentrant函数变成 reentrant函数 这个链接是说明一些non-reentrant ===> reentrant和non thread safe ===>thread safe转换的 http://www.unet.univie.ac.at/aix/aixprggd/genprogc/writing_reentrant_thread_safe_code.htm [ 本帖最后由 ypxing 于 2007-8-4 01:06 编辑 ] lenovo 回复于:2007-08-02 21:38:57 不错,很好的帖子。 科技牛 回复于:2007-08-03 15:38:14 受教很深! ypxing 回复于:2007-08-03 15:58:22 调用了malloc的函数肯定是non-reentrant的 引用:原帖由 bluster 于 2007-8-3 15:55 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7155171&ptid=971102]
最后一点是错的,比如一个函数调用malloc并不影响这个函数是否是reentrant。 ypxing 回复于:2007-08-03 15:59:35 这家伙,怎么把自己的帖子给删了? bluster 回复于:2007-08-03 16:01:11 引用:原帖由 ypxing 于 2007-8-3 15:58 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7155198&ptid=971102]
调用了malloc的函数肯定是non-reentrant的 你是对的,我一时有点绕。 其实,是对reentrant的定义有问题。 可重入的意思,差不多是函数的任意部分都可以并行,而线程安全的意思则是多线程环境下使用没有问题,对于非可重入的函数,使用lock来保护不可并行的部分从而线程安全。 引用:原帖由 ypxing 于 2007-8-3 15:59 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7155214&ptid=971102]
这家伙,怎么把自己的帖子给删了? 无价值糊涂帖,所以删了。 [ 本帖最后由 bluster 于 2007-8-3 16:05 编辑 ] jigloo 回复于:2007-08-03 16:11:55 >>3. reentrant和thread safe的本质的区别就在于,reentrant函数要求在同一个线程中需要进入两次以上, 并能正确执行. 这个说的不对,可重入区别在于允许任意中断函数的执行并恢复(比如信号) http://www.ibm.com/developerworks/cn/linux/l-reent.html 思一克 回复于:2007-08-03 17:03:49 这个问题很复杂。 LZ的帖子很好。改进的地方是LZ应该多讲WHY不可重入,如何才可重入,而不是下结论。 1)调用了不可重入函数的函数不一定是不可重入的。比如LINUX KERNEL中,设备中断处理函数是不可重入的,而__do_IRQ()调用了他们,但__do_IRQ却是可重入的。 只要保证被调用的函数部分没有重入就可以了。 2)使用的全局变量的函数也不一定是不可重入的。还比如__do_IRQ()使用了全局变量来存储数据,但它是可重入的。 类似的例子: [CODE] int ia[32]; int func(int i) { ia++; printf("%p i %d %dn", &i, i, ia); if(i == 31) return; func(i+1); } main() { func(0); } [/CODE] 关于这个问题,看LINUX中断处理部分非常有启发。那里逻辑复杂,各种重入(硬,软中断,多CPU)处理的非常巧妙。 ypxing 回复于:2007-08-03 18:50:12 思一克,你好 首先谢谢你的鼓励. 你给出的这个例子,函数func,既不是可重入的,也不是线程安全的, 原因如下: 假设有一个信号处理函数handler,里面调用了func 考虑这种情况: 主函数中调用了func(0) (这个时候,你的本意是先要ia[0]++,然后打印现在ia[0]的值, 再然后继续后面的操作), 在func刚执行完ia[0]++时,信号触发了handler函数, handler函数会调用func函数,然后执行对ia的一系列操作,完成后返回. 这时,你的主函数调用的func继续执行,也就是要printf了, 这时printf的东东就不是你想要的了,而且你无法确定现在ia[0]的值是什么(因为信号 可以中断很多次很多层).所以func不是可重入的. 而且也不是线程安全的. 可重入的一个判定方法就是将它放入信号处理函数中,仔细推敲各种中断情况下, 你是不是还能得到你想要的结果. "使用的全局变量的函数也不一定是不可重入的。"这句是正确的,只要正确使用就可以了, 但是不使用全局变量是写可重入函数的简单方法. "调用了不可重入函数的函数不一定是不可重入的。"这句是不对的, 因为你无法保证被调用的不可重入函数部分不被重入
思一克 回复于:2007-08-03 19:39:57 你写可重入函数时候要考虑到保证不可重入部分不重入, 还有保证整个函数必须可重入. __do_IRQ就是如此. 所以说"调用了不可重入函数的函数不一定是不可重入的"是正确的. 而"调用了不可重入函数的函数一定是不可重入的"是不对的.因为有十分多的反例. 调用了不可重入函数的函数不一定是不可重入的。"这句是不对的, 因为你无法保证被调用的不可重入函数部分不被重入 feasword 回复于:2007-08-03 20:09:35 一直想找这两个概念是此非彼的例子,受教了 关于死锁的问题,apue里也有讲,以前也遇到过,当时干脆都弄成递归锁了 ypxing 回复于:2007-08-03 20:49:04 那么,怎么才能保证不可重入的部分不被重入呢? 引用:原帖由 思一克 于 2007-8-3 19:39 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156157&ptid=971102]
你写可重入函数时候要考虑到保证不可重入部分不重入, 还有保证整个函数必须可重入. __do_IRQ就是如此. 所以说"调用了不可重入函数的函数不一定是不可重入的"是正确的. 而"调用了不可重入函数的函数一定是不可 ... cugb_cat 回复于:2007-08-03 22:12:05 引用:原帖由 ypxing 于 2007-8-3 20:49 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156295&ptid=971102]
那么,怎么才能保证不可重入的部分不被重入呢? 我有同楼主相同的疑问。 另外,从lz的例子中学到一些技巧,关于调试多线程程序,感谢lz。 [ 本帖最后由 cugb_cat 于 2007-8-3 22:45 编辑 ] 飞灰橙 回复于:2007-08-03 22:18:09 引用:原帖由 思一克 于 2007-8-3 19:39 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156157&ptid=971102]
你写可重入函数时候要考虑到保证不可重入部分不重入, 还有保证整个函数必须可重入. __do_IRQ就是如此. 所以说"调用了不可重入函数的函数不一定是不可重入的"是正确的. 而"调用了不可重入函数的函数一定是不可重入的"是不对的(语句A).因为有十分多的反例. 调用了不可重入函数的函数不一定是不可重入的。"这句是不对的(语句B), 因为你无法保证被调用的不可重入函数部分不被重入 越看越糊涂了,撇开讨论的问题不谈, 上面的语句A和语句B,必定有一句是错的 cugb_cat 回复于:2007-08-03 22:44:57 引用:原帖由 飞灰橙 于 2007-8-3 22:18 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156605&ptid=971102]
越看越糊涂了,撇开讨论的问题不谈, 上面的语句A和语句B,必定有一句是错的 两句意思相反~:mrgreen: ypxing 回复于:2007-08-03 23:30:14 俺也看了好一会才看懂:em02: 引用:原帖由 飞灰橙 于 2007-8-3 22:18 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156605&ptid=971102]
越看越糊涂了,撇开讨论的问题不谈, 上面的语句A和语句B,必定有一句是错的 mingyanguo 回复于:2007-08-04 00:08:35 完了,简单的问题复杂化了 :mrgreen: hakase 回复于:2007-08-08 20:37:06 好帖,受教了~~ ypxing 回复于:2007-08-08 23:05:51 这两天写了一个测试程序来验证malloc的不可重入性 但是malloc一直没有crash,有点郁闷 过段时间把自己的测试代码贴出来,让大家来帮忙看看 bluster 回复于:2007-08-09 10:08:56 引用:原帖由 ypxing 于 2007-8-8 23:05 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7176529&ptid=971102]
这两天写了一个测试程序来验证malloc的不可重入性 但是malloc一直没有crash,有点郁闷 过段时间把自己的测试代码贴出来,让大家来帮忙看看 多线程条件下,signal的handler有可能在一个单独的线程中执行,如果这样那么malloc用锁保护就够了。 ypxing 回复于:2007-08-09 10:29:51 在多线程条件下, 理论上,将malloc放入signal的handler也是会出问题的, 锁是不行的,会死锁 引用:原帖由 bluster 于 2007-8-9 10:08 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7177603&ptid=971102]
多线程条件下,signal的handler有可能在一个单独的线程中执行,如果这样那么malloc用锁保护就够了。 ypxing 回复于:2007-08-09 16:22:20 试图测试malloc不可重入性的代码如下:
验证方法是: 1. 编译main.c 和kill.c gcc main.c -o main gcc kill.c -o kill 2. 运行./main 并在另外一个终端运行ps -A|grep main查找出该进程的进程号为pid 3. 运行./kill pid (此处pid为第二步查到的pid) 运行了很长时间,也没有crash 请大家看看我的程序,讨论一个测试方案出来 引用:原帖由 ypxing 于 2007-8-8 23:05 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7176529&ptid=971102]
这两天写了一个测试程序来验证malloc的不可重入性 但是malloc一直没有crash,有点郁闷 过段时间把自己的测试代码贴出来,让大家来帮忙看看 mingyanguo 回复于:2007-08-09 17:36:23 引用:原帖由 ypxing 于 2007-8-9 16:22 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7180404&ptid=971102]
试图测试malloc不可重入性的代码如下: main.c /*这是主程序,用来调用malloc*/ #include #include #include #include #include #include void setUnblock() { sigset_t sigset; s ... 我估计是因为现在的malloc是线程安全的原因所以不会crash但是死锁。 我在debian上面的一个测试代码,会死锁,top一下会发现进程状态总是sleep
haohao06 回复于:2007-08-10 11:45:06 谢谢楼主讲解.收藏先 |
原文链接:http://bbs.chinaunix.net/viewthread.php?tid=971102 转载请注明作者名及原文出处 |
上一篇: 最混乱程序解析










文章评论
共有 位网友发表了评论 查看完整内容