Race Conditions and Reentrant and thread-safe
在多进程,多线程的环境下,由于os的调度算法,系统的负荷等诸多因素的影响,我们无法判断哪个进程先执行,哪个先程先执行。因此作为一项黄金准则是,程序必须在任何情况下都能正确的执行。任何情况大部分是指和其他进程,线程交互执行时,不能对先后执行顺序作任何假设。race condition指的就是程序的结果依赖于进程,线程的执行顺序。
比如,子进程想要打印父进程的pid程序:
int main () {
pid_t pid ;
if ( (pid = fork() ) < 0 ) {
printf(“fork error/n”) ;
return (-1) ;
} else if ( pid == 0 ) { // child
printf( “ppid: %d/n” , getppid() ) ;
return 0 ;
} else { // parent
sleep( 2 ) ; // 在此等待,为了使子进程先运行
}
}
让我们假设以下程序的执行流程:
[1] fork调用后,os首先执行父进程,父进程sleep,子进程执行,并成功打印出父进程的pid,父进程从sleep返回并退出
[2] fork调用后,os首先执行父进程,父进程sleep,os并没有调度子进程执行,父进程从sleep返回并退出后,os调度子进程执行,此时子进程的父进程将变为init进程,很明显,这不是我们的期望
[3] fork调用后,os首先调度子进程,并且成功打印出父进程的pid
[4] fork调用后,os首先调度子进程,但是在执行printf之前,os切换到父进程,并直到父进程结束后,才切换到子进程,此时子进程的父进程又变为init进程
这样的执行顺序可以很多
再看一个线程的例子:
#define NUM 10
void * thread_fun( void * parg ) {
int num = * ( (int*)parg ) ;
printf( “thread: %d/n” , num ) ;
return NULL ;
}
int main() {
pthread_t tid[NUM] ;
int i ;
for ( i = 0 ; i < NUM ; i++)
pthread_create( &tid[i] , NULL , thread_fun , &i ) ;
for ( i = 0 ; i < NUM ; i++ )
pthread_join( tid[i] , NULL ) ;
return (0) ;
}
这里共有11个线程,主线程在创建完10个线程后,等待这10个线程结束。每个线程的参数为变量i的地址。然后每个线程读取参数所指向的数据。重要的是,主线程中可以修改该地址的值(即变量i的值)。所以10个线程的输出可以有任意的可能性。
[1]理想的情况下创建线程0,线程0执行完毕printf语句时切换到主线程,其他9个线程也是同样的执行顺序,所以他们的输出是0 – 9。
[2] 但是在其他的情况下,线程0在执行前,主线程又创建了线程1,此时i已经被修改为1了,所以线程0可能就会输出1,而线程1的输出还是由线程的执行顺序决定的。我测试的几次输出为:
thread: 1
thread: 2
thread: 3
thread: 4
thread: 4
thread: 5
thread: 6
thread: 7
thread: 8
thread: 0
thread: 2
thread: 2
thread: 3
thread: 4
thread: 5
thread: 6
thread: 8
thread: 9
thread: 6
但是如果是输出parg,而不是输出 *parg的话,呵呵,那么无论怎么执行都是相同的值。因为变量i的地址是不会改变的。
蓝色的行,最后输出为6,为什么会这样呢?本以为会输出的数据会越来越大呢。这是因为从取出参数所指向的数值,到输出并不是原子的,也就是说,取出数据(6)后,线程的执行被打断(发生了context switch),一段时间后才被重新调度并输出6。所以输出10行数值,他们的值只要在0 --- 10之间随意组合都是合理的。
在多线程的环境下,如果函数被多次调用时总能产生正确的结果。我们就说函数是thread-safe的。否则就称为是thread-unsafe的。如有函数中,包含下列的任意一条,那么就是thread-unsaft的。
[1] 访问共享变量(全局变量,静态变量)时,没有使用PV原语。(没有使用mutex等作互斥访问)
[2] 依赖于跨多个函数调用的状态
[3] 返回静态变量的指针
[4] 调用thread-unsafe的函数
而reentrant函数则是一种特殊的thread-safe函数。他不引用任何的共享变量。Reentrant函数要比non-reentrant thread-safe函数效率更高,因为reentrant函数不需要执行同步(互斥)操作。有时,把thread-unsafe函数转换为thread-safe函数的唯一方法是把他重写为reentrant函数。
他们的关系如下:
Thread-safe functions
|
Thread-unsafe functions
|
如果函数的参数都是值传入的(不是通过指针和引用传入的),并且所有的数据访问都是局部stack变量(没有引用全局或静态变量),那么该函数就是explicitly reentrant函数。如果我们放松条件的话,允许传递指针类型或引用类型的参数的话,那么该函数是implicitly reentrant函数。因为如果调用这不传递指向共享变量的指针参数的话,他就是reentrant函数。所以说,reentrant性其实是调用者和被调用者双方作用出来的性质。
struct timespec * maketimeout_u( int secs ) {
static struct timespec timespec ;
struct timeval now ;
gettimeofday(&now , NULL ) ;
timespec.tv_sec = now.tv_sec + secs ;
timespec.tv_nsec = now.tv_nsec * 1000 ;
Return ×pec ;
}
因为他返回了静态变量的指针,所以是thread-unsafe的。
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;
struct timespec * maketimeout_t ( int secs ) {
struct timespec * sp ;
struct timespec * up = malloc ( sizeof(struct timespec) ) ;
pthread_mutex_lock( &mutex ) ;
sp = maketimeout_u( specs ) ;
*up = *sp ;
pthread_mutex_unlock( &mutex ) ;
return up ;
}
我们使用lock-and-copy方法写了一个thread-safe的函数。但是他不是reentrant的,因为访问了共享的变量。
struct timespec * maketimeout_r( struct timespec * tp , int secs ) {
struct timeval now ;
gettimeofday( &now , NULL );
tp->tv_sec = now.tv_sec + secs ;
tp->tv_nsec = new.tv_nsec * 1000 ;
return tp ;
}
现在该函数是implicitly reentrant的。但是调用者不放将指向共享变量的指针传入maketimeout_r函数,否则的话,他就是thread-unsafe函数。