您的位置:首页 > 其它

可重入和线程安全

2016-02-09 12:43 204 查看

前言

在上一篇博客,即笔者翻译的文章《线程和QObjects》中,发现了一个疑问。文章的开头部分提出“QObjects和它的大多数非GUI子类都是可重入的,这使得在多线程中同时使用它们成为可能”,而文中第三段又明确提出“QObject和它所有的子类都不是线程安全的”。这让人比较有疑问,难道可重入的还不是线程安全的吗?于是去网上查询了大量的资料,其实主要是近二十篇文献,也包括百度百科。结果却发现,除了有重复转载外,不少博客的中文用语并不清晰,有的甚至有混乱和错误。笔者将多篇比较有价值的文章复制下来之后,逐字逐句一一分析,去繁就简,改变文风,整理了一番。这其中,学到了不少知识,感谢这些作者。另外,却也发现了一个少为人论述的问题,就是:可重入可能并不是一个严谨的说法,更严谨的说法是多线程可重入和单线程可重入。具体请参见下文。当然,这也是笔者的一家之见,因为之前几乎没什么人专门论述过,因此也请读者多多指教。在下文中,将列出网上整理总结的十多篇文献的精华部分。因结构已完全改变,而语言文法也大变样了,故厚颜为“原创”。

第一节论述线程安全,这一节相对简单且没有什么易引起争论的地方;

第二节论述可重入函数,在这一节中将重点提出和总结单线程可重入和多线程可重入的概念,这一节的最后给出对多个概念的比较,另外也列出了一些笔者尚未特别明白的问题;

第三节是最后一节,这一节里只是记录一些知识点。

线程安全

线程安全:如果一个函数在同一时刻可以被多个线程安全地调用,就称该函数是线程安全的。所谓安全,就是程序最终结果正确。

线程安全,指的是函数能同时被多个线程安全地调用,并不要求在某一线程中该函数的执行结果没有二义性。

举例,开N个线程对初始为0的全局变量进行+1操作,直至全局变量为1000,在每次+1操作后打印本线程内的+1后的结果。因为线程安全的函数对全局变量进行了互斥保护,所以最终结果总是会正确,但具体到每个线程,它们打印出的数字却是不确定的。

线程安全问题都是由共享的资源,如全局变量、静态变量、IO等引起的。

一个函数如果不需要访问共享资源,那么为调用该函数的每个线程提供数据副本即可,该函数是线程安全的;
一个函数如果访问的是只读的共享资源,那么该函数是线程安全的;
一个函数如果访问的是非只读的共享资源,那么需要利用互斥锁等提供线程同步,以确保程序以确定的方式(即串行)操作。

可以通过互斥锁等机制,将不可重入函数转换成线程安全的函数。

可重入

可重入函数简单来说就是可以被中断而不会引起执行结果有二义性的函数。
在APUE中,函数可重入的概念最先是在讲signal的handler的时候提出的。某进程或线程正在执行函数fun(),突然接收到一个信号sig, 此时需要暂停执行fun(),转去执行sig信号的处理函数sig_handler(),而有可能在sig_handler()中也调用了函数fun()。当sig_handler()执行结束后,CPU会从原来fun()被打断的地方继续往下执行。如果fun()函数是可重入的,那么多次调用fun()函数的执行结果没有二义性,从而2次调用fun()的结果都是正确的预期结果。

如果一个函数只使用自己栈上的变量而不依赖于任何资源,那就是纯代码可重入函数。可以允许在多线程时每个线程运行一个副本(即多线程可重入),因为它们使用的是各自的栈,所以不会互相干扰。

为了保证函数是可重入的,需要做到以下几点:

不在函数内部使用静态或者全局数据
不返回静态或者全局数据
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
如果必须访问全局数据,使用互斥锁来保护
不调用不可重入函数(如malloc()函数和标准IO函数等)

可重入函数分为单线程可重入和多线程可重入。(之前网上似乎没有人强调过这个概念。)

一个函数在单一个线程中中断进入多次,执行结果没有二义性(即“多次调用、结果一致”),那么就是单线程可重入的。
如果一个函数对信号处理程序的重入是安全的,那么该函数是“异步信号安全”的。(其实,信号在本质上就是异步的。)
单线程可重入函数,一定是异步信号安全的。
多线程可重入函数,不一定是异步信号安全的。所以,多线程可重入函数,不一定是单线程可重入函数。
多线程可重入函数,一定是线程安全的。(根据逆否定理,不是线程安全的函数一定不是多线程可重入的。)

问题:(括号中的结论为笔者个人观点,尚无证实或证伪)

1. 单线程可重入函数,是否一定是多线程可重入的?(不一定)

2. 线程安全函数,是否一定是多线程可重入的?(不一定)

3. 线程安全函数,是否一定是单线程可重入的?(不一定)

4. 异步信号安全的函数,是否一定是单线程可重入的?(是,结合上面,因此,单线程可重入和异步信号安全其实是等价的。)

其他

即使对于可重入函数,在信号处理函数中使用也需要注意一个问题就是errno。一个线程中只有一个errno变量,信号处理函数中使用的可重入函数也有可能会修改errno。例如,read函数是可重入的,但是它也有可能会修改errno。因此,正确的做法是在信号处理函数开始,先保存errno;在信号处理函数退出的时候,再恢复errno。
程序正在调用printf输出,但是在调用printf时,出现了信号,对应的信号处理函数也有printf语句,就会导致两个printf的输出混杂在一起。如果是给printf加锁的话,会导致死锁。对于这种情况,采用的方法一般是在特定的区域屏蔽一定的信号。
malloc()函数是一个典型的不可重入但是线程安全的函数。因为malloc()使用静态数据结构来记录哪些内存块是空闲的。free()函数和malloc()函数类似。
strtok既不是可重入的,也不是线程安全的;加锁的strtok不是可重入的,但线程安全;strtok_r既是可重入的,也是线程安全的。
操作系统实现支持线程安全函数的时候,会对POSIX.1中的一些非线程安全的函数提供一些可替换的线程安全版本。例如,gethostbyname()是线程不安全的,在Linux中提供了gethostbyname_r()的线程安全实现。函数名字后面加上"_r",以表明这个版本是可重入的(对于线程可重入,也就是说是线程安全的,但并不是说对于信号处理函数也是可重入的,或者是异步信号安全的)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: