线程的安全和可重入(待续)
POSIX线程函数一共有110多个,常用的不过十几个。贵精不贵多。这11个最基本的Pthread函数是:
2个:线程的创建和等待结束(join)。
4个:mutex的创建、销毁、加锁、解锁。
5个:条件变量的创建、销毁、等待、通知、广播。
用这三类东西(thread、mutex、condition)可以完成任何多线程任务(至少对于我经历过的项目)。学习多线程的难点不在于学习线程的原语,而在于理解多线程与现有的C/C++库函数和系统调用的交互关系。以进一步学习如何设计并实现线程安全的程序。要理解一个线程对某个共享变量的修改何时能被其他线程看到,这称为内存能见度。
线程安全
线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。(来自维基百科)
一般来说,线程安全的函数应该为每个调用它的线程分配专门的空间,来储存需要单独保存的状态(如果需要的话),不依赖于“线程惯性”,把多个线程共享的变量正确对待(如,通知编译器该变量为“易失(volatile)”型,阻止其进行一些不恰当的优化),而且,线程安全的函数一般不应该修改全局对象。
线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。
产生线程不安全问题在于多个线程访问了相同的资源,如同一片内存区(数组、文件等),实际上,只要没有写操作,多线程访问同一共享变量是线程安全的。
例子:
在下面这段代码中,函数increment_counter是线程安全的,但不是可重入的。
1 #include <pthread.h> 2 3 int increment_counter () 4 { 5 static int counter = 0; 6 static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 7 8 pthread_mutex_lock(&mutex); 9 10 // only allow one thread to increment at a time 11 ++counter; 12 // store value before any other threads increment it further 13 int result = counter; 14 15 pthread_mutex_unlock(&mutex); 16 17 return result; 18 }
函数increment_counter可以在多个线程中被调用,因为有一个互斥锁mutex来同步对共享变量counter的访问。因此是线程安全的。但是如果在pthread_mutex_lock(&mutex)和pthread_mutex_unlock(&mutex)之间产生另一个调用函数increment_counter的中断,会第二次执行此函数,由于mutex已经被lock了,还没有解锁,此时第二个函数会在pthread_mutex_lock(&mutex)处阻塞,由于mutex没有机会被unlock,阻塞会永远持续下去。简言之,问题在于 pthread 的 mutex 不可重入。
解决办法是设定 PTHREAD_MUTEX_RECURSIVE 属性。然而对于给出的问题而言,专门使用一个 mutex 来保护一次简单的增量操作显然过于昂贵,因此c++11中的原子变量提供了一个可使此函数既线程安全又可重入(而且还更简洁)的替代方案:
1 #include <atomic> 2 3 int increment_counter () 4 { 5 static std::atomic<int> counter(0); 6 7 // increment is guaranteed to be done atomically 8 int result = ++counter; 9 10 return result; 11 }
保证线程安全的方法主要是如果要修改共享变量,使用同步原语:互斥锁、条件变量、信号量。线程的局部变量都是线程安全的。