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

原创 2018年04月17日 19:55:06

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

ADO.NET深入研究(2)[特别推荐]

导 读:.NET 在数据存取方面做了很大的调整。在.NET 框架下,数据存取是由ADO.NET来完成的,这是一个ADO的改进和完善版本。它最显著的变化是其完全基于XML。而对于从事ADO开发的人员来说...
  • coolstar
  • coolstar
  • 2001-05-05 14:01:00
  • 811

关于多线程导致数据不一致的情况的思考

这里我要引入一个比较不是很常见的Java 内存模型(JMM java Memory Model),线程基础数据会存放在一个自身对应的线程栈中,如果两个线程需要交互必须要通过共享内存中的变量进行。才能够...
  • zsf5201314z
  • zsf5201314z
  • 2016-11-30 11:37:04
  • 1268

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

1.线程共享全局变量 在学习线程的相关概念之后,想探究在进程的虚拟地址空间当中的哪些区域是进程中多个线程共享的。 探究发现,全局变量在不同的线程当中访问全局变量是共享的。举例如下: #inclu...
  • ASJBFJSB
  • ASJBFJSB
  • 2018-04-17 19:55:06
  • 30

python进阶之多线程对同一个全局变量的处理

通常情况下: from threading import Thread global_num = 0 def func1(): global global_num for i in range...
  • m0_37338590
  • m0_37338590
  • 2017-11-08 18:24:48
  • 2249

在子线程中改变一个全局变量,然后再通知主线程

public delegate void SetIntValue(int value); public void setX(int value) { if (InvokeRequired)...
  • hutao1101175783
  • hutao1101175783
  • 2013-07-30 20:54:58
  • 2801

python中的多线程-共享全局变量

在一个进程内的所有线程共享全局变量,能够在不适用其他方式的前提下完成多线程之间的数据共享(这点要比多进程要好) 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全) ...
  • xun527
  • xun527
  • 2017-08-10 22:59:35
  • 1191

多线程

1,多进程并发 在一个应用程序中使用并发的第一种方法,是将应用程序分为多个、独立的、单线程的进程,它们运行在同一时刻,就像你可以同时进行网页浏览和文字处理。这些独立的进程可以通过所有的常规的进程...
  • lusic01
  • lusic01
  • 2017-10-23 11:29:23
  • 222

多线程题目

1、基本概念         详见:线程和进程关系和区别、同步和互斥、进程间通信 2、以下多线程对int型变量x的操作,哪几个不需要进行同步(D)         A. x=y;      B. ...
  • weiyuefei
  • weiyuefei
  • 2016-07-23 14:21:11
  • 656

C++多线程系列(一)

    1,并发编程        并发编程由两种模型,第一中模型是多进程,第二种模型使多线程。       进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。 在同一个时间里...
  • wangjianbo09
  • wangjianbo09
  • 2018-04-21 13:35:06
  • 3

关于多线程并发:每个开发人员都应了解的内容

本文讨论:   多线程和共享内存线程模型争用及并发访问如何能够打破不变量作为争用标准解决方案的锁定何时需要锁定如何使用锁定;理解开销锁定如何能够各行其道   十年前,只有核心系...
  • HMSIWTV
  • HMSIWTV
  • 2012-08-27 09:19:00
  • 14514
收藏助手
不良信息举报
您举报文章:多线程访问共享的全局变量引发的数据混乱
举报原因:
原因补充:

(最多只允许输入30个字)