一. 线程安全

    前面提到过线程的同步与互斥,也就是当两个线程同时访问到同一个临界资源的时候,如果对临界资源的操作不是原子的就会产生冲突,使得结果并不如最终预期的那样,比如如下的程序:

#include <stdio.h>
#include <pthread.h>

int g_val = 0;

void* fun(void *arg)
{
    int i = 0;
    while(i++ < 500)
    {   
        int tmp = g_val;
        printf("thread is :%u        g_val: %d\n", pthread_self(), g_val);
        g_val = tmp + 1;; 
    }   
}

int main()
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, fun, NULL);
    pthread_create(&tid2, NULL, fun, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("g_val: %d\n", g_val);

    return 0;
}

上面创建了两个线程,我们知道,同一个进程中的线程之间对进程的资源是共享的,当两个线程进入同一个临界区将一个全局变量g_val的值进行不断地加1的时候,因为这个操作过程并不是原子的,因此就会产生访问冲突,本来我们是想让两个线程对g_val各自加500次,最终结果想要输出1000,但运程程序我们会发现:

wKioL1ctpGKieSUZAAAfnXnrwJg899.png

结果并不如我们预期的那样,而且每一次运行的结果都不一样;因此,这种代码我们称作不是线程安全的;

    因此,线程安全是指当多个线程访问同一个区域的时候其最终的结果是可预期的,并不会因为产生冲突或者异常中断再次恢复而使结果不可预期

-------------------------------------------------------------------------------------------

二. 可重入函数

    当程序运行到某一个函数的时候,可能会因为硬件中断或者异常而使得在用户态正在执行的代码暂时中断转而进入内核,这个时候若有一个信号需要被处理,而这个处理这个信号的时候又会重新调用刚才中断的函数,如果函数内部和上面的栗子一样有一个全局变量需要被操作,那么,当信号处理完成之后重新返回用户态恢复中断函数的上下文再次继续执行的时候,对同一个全局变量的操作结果可能就会发生改变而并不如我们预期的那样,这样的函数被称为不可重入函数;

    相对应的,当一个执行流因为异常或者被内核切换而中断正在执行的函数而转为另外一个执行流时,当后者的执行流对同一个函数的操作并不影响前一个执行流恢复后执行函数产生的结果,我们就称这个函数为可重入函数。

下面举个栗子:

#include <stdio.h>
#include <signal.h>

int g_val = 0;

void fun()
{
    int i = 5;
    while(i--)
    {   
        g_val++;
        printf("g_val: %d\n", g_val);
        sleep(1);
    }   
}

int main()
{
    signal(2, fun);
    fun();
    printf("when the fun end, g_val: %d\n", g_val);

    return 0;
}

上面的程序中首先在main函数中注册了一个函数fun,signal函数意思是当收到2号信号时执行自定义的函数也就是fun,而2号函数是由Ctrl+c产生的SIGINT信号,该信号的默认处理动作是终止进程,这里我们捕捉它;

设置了一个全局变量g_val,fun函数的功能是每隔1秒将g_val进行加1,最终是想让g_val的值为5,而运行这个程序:

wKioL1ctsPuzkuF-AAAldAHvgOs843.png

上面的结果中,当g_val加到3的时候,键入Ctrl+c,这时进程会收到2号信号,而2号信号会被捕捉到执行fun函数,因为g_val是全局变量,因此执行捕捉信号的函数会继续在全局变量g_val上相加,因此,结果为10;也就是上面的fun函数是不可重入的;


为了使结果并不因为中断而改变,可将程序改为如下:

#include <stdio.h>
#include <signal.h>

void fun()
{
    int val = 0;
    int i = 5;
    while(i--)
    {   
        val++;
        printf("val: %d\n", val);
        sleep(1);
    }
}

int main()
{
    signal(2, fun);
    fun();

    return 0;
}

上面的函数将val设定为局部变量,因此,就算最终函数被中断或者切出,当回来继续执行时,最终结果并不会改变;

wKiom1ctsCOyi_JpAAAJBX50Fos699.png

结果中,当函数两次被调用的时候,两次结果最终并不影响且值相等;所以,上面的程序中,函数fun是可重入函数;

    一个可重入函数需要满足的是:

  1. 不使用全局变量或静态变量;

  2. 不使用用malloc或者new开辟出的空间;

  3. 不调用不可重入函数;

   其实总结就是,一个可重入函数内部使用的数据都应该来自于自身的栈空间,包括返回值也不应该是全局或者静态的;而正是因为其中的操作数据都来自于自身的栈空间,而每次调用函数会开辟不同的栈空间,因此二者互不影响。

-------------------------------------------------------------------------------------------

三. 可重入函数与线程安全的区别与联系

  • 线程安全是在多个线程的情况下引发的,而可重入函数可以是在一个线程情况下而言的;

  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的;

  1. 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的;

  2. 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果重入函数的话若锁还未释放则会产生死锁,因此不是可重入的;

  3. 如果一个函数当中的数据全是自身栈空间的,那么这个函数既是线程安全也是可重入的;

  • 线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作互不影响使结果是相同的。



《完》