Race Conditions and Reentrant and thread-safe

 
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: 9
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
Reentrant 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 &timespec ;
}
因为他返回了静态变量的指针,所以是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函数。
 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值