编写线程安全的代码
新近参与的一个搜索项目,上线前的压力测试时,会导致后台程序不稳定,偶尔程序会崩溃掉,在debug模式下却很难重现。现场一片狼藉,除了STL和pthread那些残迹,基本上很难得到有用信息。在经历一周多的排查后,终于发现了bug的藏身之处。下面代码即是浪费我们大量时力的bug源头,拿来做楔子吧:
1 | template<class T> |
这段代码通常来看不会有什么问题,而且能很好地完成任务。但当它放在多线程环境中时,大量请求会并发地调用该函数进行字符串的切分。这个时候,这个函数的问题就大条了,在高并发时会破坏内存,让程序内部直接爆掉。而原因是因疏忽调用了strtok——线程不安全的系统调用,使得该函数是线程不安全的。线程不安全的代码,会带来灾难,这也是我写这篇博客的原因。
在编程中,我们常常需要编写多线程的程序,而在编写这类程序的时候,则需要特别注意函数的线程安全性。如果不小心编写或引入了线程不安全的函数,往往会带来令人头大的后果。它们隐蔽并且行为飘忽,轻则让你调试数天,重则导致服务的不可预知行为。
在说线程安全之前,我们可以提提可重入这个概念,可重入有很多正统的解释,简单地说,一个函数可以在多次调用执行进入或者挂起时,不影响和改变其行为和结果,则为可重入的。
那么什么是线程安全函数呢,概念上比较直观,当多个并发线程反复调用时,它不会因为同时调用而相互影响,改写对方的数据和状态,可以保证结果的正确性。
可重入和线程安全函数的概念很像,但并不一致,可重入的函数一定是线程安全的,但线程安全的并一定是可重入的函数。比如说,对于一个内部加锁的不可重入函数,其可以是线程安全的。但这种安全性并不值得提倡,会带来一些互斥问题的坏处(死锁,低效),本文中所说到的线程安全编程,一般优先考虑编写和调用可重入的函数。另外还有一个异步信号安全的概念,其对函数的要求更高,但暂不在本文的讨论范围之内。
要确保线程安全,主要需要考虑该函数在是否存在共享变量,有共享就可能冲突。以下的行为往往会引入一下共享的变量进来,从而导致线程不安全:
1. 读写了共有数据(静态,全局,成员),若该静态或全局数据是可写的且有函数写入,则该行为会导致线程不安全。
2. 调用了线程不安全的自有或系统函数,则会导致线程不安全。
3. 使用了线程不安全的数据结构作为参数,若该数据结构可写,则会导致线程不安全。
在编程中,我们需要要求自己去注意自己的函数的可重入性,避免上述三点,避免共有数据的写操作。这里面特别需要注意的是我们对基础库及系统函数的使用,容易因为不熟悉或者疏忽而在多线程环境中误调用线程不安全的函数及数据结构,给自己的程序埋下隐患。
- 一个小例子
1 | #include <iostream> |
以上是一个测试小程序,用来演示在多线程环境下对STL string的一种误用。该测试情况下会可能引发重复析构的问题。如下:1
bad_nameis wrong vs bad_name
bad_nameis wrong vs good_name
*** glibc detected *** ./thread_safety: double free or corruption (fasttop): 0x0937b3b8 ***
*** glibc detected *** ./thread_safety: double free or corruption (fasttop): 0x0937b3b8 ***
bad_nameis wrong vs good_name
*** glibc detected *** ./thread_safety: double free or corruption (fasttop): 0x0937b3b8 ***
bad_nameis wrong vs bad_name
bad_nameis wrong vs bad_name
======= Backtrace: =========
/usr/lib/i386-linux-gnu/libstdc++.so.6(_ZdlPv+0x1f)[0xb769f51f]
/lib/i386-linux-gnu/libc.so.6(+0x75ee2已放弃 (核心已转储)
原因是因为大部分的STL数据结构并不是线程安全的,对string, map, vector等典型结构的读写并发若不加锁保护,会引发内部结构变量的冲突。本例正是如此,string的常见实现是利用引用计数来节省内存并加速性能,而在读写时,有可能引发这种计数机制的混乱,从而导致以上问题。其实不仅仅是STL类,很多类甚至基础类型都不能保证读写安全,故在多线程实现时需要多多注意。
- 常用的系统函数和数据结构里面,有以下几类是线程不安全的:
- STL的基本数据结构string, vector,list,queue等都是线程不安全的,或许在某些平台的实现上是线程安全的,但规范上并不保证这一点。故我们在使用时,尤其要小心,尽量保证对这些数据结构的操作安全。
- 因为C的历史原因,有一系列常用的系统调用是线程不安全的,它们现在一般都已有可重入的实现(xxfunc_r),主要有以下几类:
1) 时间函数localtime, gmtime, asctime等都不是线程安全的,它们使用了静态缓存来返回结果,在多线程环境下会导致冲突,可考虑替代的实现_r。
2) 几乎所有的IO函数都不是线程安全的,它们IO的对象往往是共有对象(文件,终端),在多线程环境下会出现问题,可考虑必要的加锁实现或者单一工作线程的方式。
3) 字符串处理函数strtok, 该函数在内部使用静态数据来记录字符串中下一个需要解析的标记的位置,在多线程环境下会出现问题。
4) 网络函数gethostbyaddr, gethostbyname等,使用了静态缓存来存储并返回结果,在多线程环境下会出现问题。
Tips
malloc(), free()是线程安全的么?
- 之前的crt实现不是,现在是,在linux平台下的glibc已经支持线程安全(内部锁),而windows平台也通过多线程CRT支持到。
system()是线程安全的么?
- 文献5中说明,system()不需要线程安全,而POSIX标准把其列为线程不安全函数,待后续分析。
附件
/article/tech-write_thread_safe_code/编写线程安全的代码.pptx参考:
[1] POSIX线程不安全函数 http://www.ccvita.com/506.html
[2] Thread-safe functions http://kernel.org/doc/man-pages/online/pages/man7/pthreads.7.html
[3] malloc 的可重入性 http://www.chinaunix.net/old_jh/23/942090.html
[4] system函数说明 http://pubs.opengroup.org/onlinepubs/9699919799/functions/system.html
[5] 编写可重入和线程安全的代码 http://blog.csdn.net/lovekatherine/article/details/1544585
线程不安全函数列表:
1 | POSIX.1-2001 and POSIX.1-2008 require that all functions specified in the standard shall be thread-safe, except for the following functions: asctime() basename() catgets() crypt() ctermid() if passed a non-NULL argument ctime() dbm_clearerr() dbm_close() dbm_delete() dbm_error() dbm_fetch() dbm_firstkey() dbm_nextkey() dbm_open() dbm_store() dirname() dlerror() drand48() ecvt() [POSIX.1-2001 only (function removed in POSIX.1-2008)] encrypt() endgrent() endpwent() endutxent() fcvt() [POSIX.1-2001 only (function removed in POSIX.1-2008)] ftw() gcvt() [POSIX.1-2001 only (function removed in POSIX.1-2008)] getc_unlocked() getchar_unlocked() getdate() getenv() getgrent() getgrgid() getgrnam() gethostbyaddr() [POSIX.1-2001 only (function removed in POSIX.1-2008)] gethostbyname() [POSIX.1-2001 only (function removed in POSIX.1-2008)] gethostent() getlogin() getnetbyaddr() getnetbyname() getnetent() getopt() getprotobyname() getprotobynumber() getprotoent() getpwent() getpwnam() getpwuid() getservbyname() getservbyport() getservent() getutxent() getutxid() getutxline() gmtime() hcreate() hdestroy() hsearch() inet_ntoa() l64a() lgamma() lgammaf() lgammal() localeconv() localtime() lrand48() mrand48() nftw() nl_langinfo() ptsname() putc_unlocked() putchar_unlocked() putenv() pututxline() rand() readdir() setenv() setgrent() setkey() setpwent() setutxent() strerror() strsignal() [Added in POSIX.1-2008] strtok() system() [Added in POSIX.1-2008] tmpnam() if passed a non-NULL argument ttyname() unsetenv() wcrtomb() if its final argument is NULL wcsrtombs() if its final argument is NULL wcstombs() wctomb() |
Written by Steve Lee(llwgod@gmail.com), 2013-08-10