多线程访问共享的全局变量引发的数据混乱

1.线程共享全局变量

在学习线程的相关概念之后,想探究在进程的虚拟地址空间当中的哪些区域是进程中多个线程共享的。
探究发现,全局变量在不同的线程当中访问全局变量是共享的。举例如下:

#include<stdio.h>
#include<assert.h>
#include<pthread.h>//线程库

char *str;//定义指向字符串的全局变量str

//线程函数
void* my_fun(void *arg)
{
    printf("函数线程:str = %s\n",str);
    //若共享,输出为主线程修改指向后指向的字符串,否则会出现段错误。
    return NULL;
}

int main(void)
{
    pthread_t id;//传出参数,用于保存成功创建线程后对应线程的id
    int res = pthread_create(&id,NULL,my_fun,NULL);
    //成功创建返回值为0
    assert(0 == res);
    str = "hello";//修改全局指针变量的指向
    pthread_exit(NULL);//退出当前线程
    return 0;
}

测试结果
这里写图片描述
可见,全局变量在多个线程中是共享的。

2.多线访问共享变量引发的数据混乱。

虽然线程共享全局变量相对于进程通信会给线程通信带来巨大的方便,但是探究以下问题时发现不做控制的进行访问全局变量也是致命的,带来巨大程序bug,并且难以发现,首先请看一下代码:

#include<stdio.h>
#include<assert.h>
#include<pthread.h>
#define MAX 10000

int count = 0;//定义全局count并初始化为0

//函数线程A
void* my_funa(void *arg)
{
    int i = 0;
    for(;i < MAX;++i)
    {
        int cur = count;
        cur++;
        count = cur;
        printf("线程A:id = %lu,count = %d\n",pthread_self(),count);
        usleep(10);
    }
    return NULL;
}

//函数线程B
void* my_funb(void *arg)
{
    int i = 0;
    for(;i < MAX;++i)
    {
        int cur = count;
        cur++;
        count = cur;
        printf("线程B:id = %lu,count = %d\n",pthread_self(),count);
        usleep(10);
    }
    return NULL;
}

//解释:定义全局变量count并初始化为0作为计数器
//在函数线程A和函数线程B分别进行10000次的++操作
//那么在两个线程执行完毕之后此时计数器count的值为20000
//usleep(10),是为了模仿交替执行的过程,主动放弃cpu的执行权。

int  main()
{
    //创建两个线程
    pthread_t pthid1,pthid2;
    int res1 = pthread_create(&pthid1,NULL,my_funa,NULL);
    int res2 = pthread_create(&pthid2,NULL,my_funb,NULL);
    assert(0 == res1);
    assert(0 == res2);

    //阻塞,回收函数线程资源
    pthread_join(pthid1,NULL);
    pthread_join(pthid2,NULL);
    return 0;
}

使用gcc编译并执行:
这里写图片描述
多次执行结果:
这里写图片描述

这里写图片描述

这里写图片描述

这里结果就令我感到非常疑惑,按照我们预期的结果。在两个线程中,都访问了全局变量并且同样进行了一万次的++操作,结果应该是20000。但是在这里我们看到多次执行结果每次的输出并不一致,存在结果为20000的情况,但是更多的是小于20000的情况。
试想一下,这样的程序应用在实际的软件当中,必然会带来巨大的漏洞和危害,造成一定的经济损失。

下面就开始探索如何这样的问题是如何出现的?为什么执行同一个可执行程序不能得到相同的计算结果?该如何解决这样的问题?

通过查阅相关的资料,得到了一下的信息。
(1)时间片轮转技术
(2)cur++的反汇编代码

时间片轮转技术
什么是时间片轮转技术?在计算机发展的早期,CPU的价格昂贵,如果执行一个程序时但其输入输出需要的时间比较长,此时CPU就必须等到数据的到来才能进行运算。对于这样的时间浪费,在那个时期简直就是暴殄天物。聪明的计算机前辈很快就意识到这个问题,于是提出了监控CPU状态的程序。当发现CPU处于等待IO时,切换到等待获取CPU执行的程序,使得CPU被充分利用起来,这就是最早期的多道程序设计的思想。但是这样的调度策略显得太过粗糙,不分程序之间的优先级。随后就提出了分时系统的调度策略,即每个程序执行一小段时间之后将CPU的控制权交给其他就绪的程序。使得每个程序都有得到使用CPU执行的机会。这大概就是时间轮转技术的雏形吧。

现代计算机的时间片轮转技术是这样定义的:
在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则,排成一个队列,每次调度时,把CPU分配给队首进程,并令其执行一个时间片。时间片的大小从几ms到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程,在一给定的时间内,均能获得一时间片的处理机执行时间。
如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。

在这里为什么要提时间片轮转技术呢?针对我们上边的线程访问全局变量时,分配给单个线程执行时间是有限的,而且为了模仿交替执行的过程,程序中还使用了usleep(10)系统调用函数,主动交出CPU的控制权。但其实在拥有CPU控制权的那段时间内线程只能执行有限的指令条数,但这与输出结果不一致有什么关系呢?

先看下边cur++的反汇编代码:
这里写图片描述
可以看到++过程是在寄存器中进行的。到这里可以说问题已经解决了。
试想下面一个过程。
(1)时间片分给线程A执行代码,当cur在寄存中中累加到了100,此时恰巧时间片被用完了,而存放在寄存器中的中间变量还没来及写入实际的物理内存。

(2)时间片分配给线程B,由于线程A算出来的值并没有写回内存,所以实际上此时线程B还是取得 cur == 0 而进行的 ++ 操作,大概进行了 200次++ 操作,但这次时间片刚好够用,线程B将得到的 cur == 200 写回了实际的物理内存。

(3)时间片再度分配给线程A,线程A开始执行它在上一个时间片结束时没有执行完的工作,将 cur == 100 写入实际的物理内存,计算机严格按照代码执行指令,殊不知此时会将由线程B计算出来的 cur == 200 覆盖,这就是为什么在上面的图片当中,三次执行相同的代码,得到的结果完全不一致的原因。

当然这就牵扯到了线程安全以及线程同步的问题,在后边的文章中会提到,本人理解目前的能力也就只能理解到这里,希望大佬再次留下您的解答,感激不尽。

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ASJBFJSB/article/details/79977886
个人分类: linux
上一篇线程的概念及linux下线程库相关函数的使用
下一篇子进程继承父进程的锁
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭