专栏名称: Linux内核之旅
Linux内核之旅
目录
相关文章推荐
Linux就该这么学  ·  Linux ip命令常用操作 ·  昨天  
Linux就该这么学  ·  4 名程序员被捕、维护赌博网站月薪最高 ... ·  昨天  
Linux就该这么学  ·  输入中文秒变运维代码的神器,竟还接入满血 ... ·  2 天前  
Linux就该这么学  ·  不关闭=危险?这 30 个服务高危端口请牢记!! ·  2 天前  
Linux就该这么学  ·  反内卷!多家公司禁用 PPT ·  3 天前  
51好读  ›  专栏  ›  Linux内核之旅

由一个线程例子引发的思考

Linux内核之旅  · 公众号  · linux  · 2017-10-13 09:21

正文

原文链接:http://blog.csdn.net/lyh__521/article/details/49759111

在谈这个例子之前先贴上进程与线程的内存结构,方便对线程有一个更深的理解。( 如果觉得前面的介绍很烦,可以直接跳到最后看问题的分析和最终解决方法的代码


进程的内存结构

下图是在Linux/x86-32中典型的进程内存结构,从图中的地址分布可以看出,内核态占1G空间,用户态占3G空间

关于进程的虚拟地址空间可以参考: http://blog.csdn.net/slvher/article/details/8831885
更详细的了解,可以查阅《深入理解计算机系统》虚拟存储器章节和《操作系统教程–Linux实例分析》。

拥有2个线程的进程的内存空间

下图没有画出内核态


可以看出线程和进程的一个明显的区别, 线程内存空间并不会独立于创建他的进程,线程是运行在进程的地址空间中的。同一程序中的所有线程共享同一份全局内存区域,其中包括初始化数据段,未初始化数据段,以及堆内存段。


线程例子

这个程序想要实现的是:
计算3个线程总共循环了多少次,main_counter 是直接计算总的循环次数,counter[i] 是计算第 i 号线程循环的次数。sum 是3个线程各自循环次数的总和。所以,理论上main_counter 和 sum 值应该是相等的,因为都是在计算总循环次数。

代码1:

#include#include#include#include#include#include#define MAX_THREAD  3  //线程个数unsigned long long   main_counter,counter[MAX_THREAD]={0};void* thread_worker(void*  arg)
{//将指针先强转为int* 再赋值 
    int  thread_num = *(int*)arg;//    printf("thread_id:%lu   counter[%d]\n",pthread_self(),thread_num);
    for(;;)
    {
        counter[thread_num]++;    //本线程的counter 加 1
        main_counter++;
    }
}int main(int argc,char* argv[])
{    int                 i,rtn,ch;
    pthread_t           pthread_id[MAX_THREAD] =  {0};  //存放线程
    for(i=0;i//传 &i 
        pthread_create(&pthread_id[i],NULL,thread_worker,&i);
    }    do
    {        unsigned long long   sum = 0;        for(i=0;iprintf("No.%d: %llu\n",i,counter[i]);
        }        printf("%llu/%llu\n",main_counter,sum);
    }while((ch = getchar())!='q');    return 0;
}
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

  • 25

  • 26

  • 27

  • 28

  • 29

  • 30

  • 31

  • 32

  • 33

  • 34

  • 35

  • 36

  • 37

  • 38

  • 39

  • 40

  • 41

  • 42

  • 43

  • 44

  • 45

  • 46

这个程序执行后,加上主线程共有4个线程在运行,子线程执行的都是thread_worker 函数中的内容:

在这块,其实对于子线程共享了主线程的哪些资源,不必死记硬背。既然子线程运行的是函数中的内容,我们不妨就把子线程的运行想象成在调用函数。只是与我们平时写的单线程的程序不同的是,thread_worker 函数被调用了3次,而且3个函数在同时被执行。main_counter 和 counter[] 数组是全局的,所以3个子线程可以直接使用和改变它们。而thread_num 是函数内的局部变量,所以线程之间互相不可见。


下来看看在这个程序中我们可能会遇到哪些问题?

问题1 传参很诡异

原因: 传参被主线程破坏

分析:
正像上面代码中那样,我们在创建线程的时候习惯于传指针或取地址进去,即 &i :

 pthread_create(&pthread_id[i],NULL,thread_worker,&i);
  • 1

这时发现运行结果是这样的:

很奇怪,0号线程和1号线程的循环次数是0,多执行几次发现经常会有线程循环次数为0,但是3个线程分明都被创建成功了,不可能不执行for 循环。

将代码1 函数中的注释去掉,我们打印一下thread_num 的值是否正常,同时打印线程ID用于区分线程:

居然没有1号线程打印的 counter[i] ,但是打印3个thread_id 值不同,说明线程1也在运行。只是因为 i = 1 传入线程函数后,thread_num 却变成了2,导致最后线程1 和 线程2 都是在对 counter[2] 执行加法操作。看来传参过程出现了问题。

看一下参数传递的具体过程:

很明显,当线程在执行thread_num的赋值操作之前很有可能因为时间片用完将CPU控制权交给其他线程或者此时有其他线程在同时运行(多核CPU)。

当传 &i 进去时,可能会发生以下情况:

如上图,如果在赋值之前,主线程进行了下一次for 循环,执行 i++ ,准备创建 1 号线程时,*arg 变成了 1 。因为 &i = arg = 0x6666,它们对应的是同一块内存。


对了,上述代码还有可能会出现3个线程传过去的参数都变成0的情况,起初很不解,参数应该只会偏大不应该比真实值小啊。在谷仕涛同学的提醒下,终于找到了原因,源头在这块代码:

当主线程很快的执行完44~47的for循环,马上又进入49~行的do while 循环中,并且执行了52行for 的第一次循环时,i 被赋值为了0,此时就有可能导致thread_worker 函数中的 *arg 变为了0,使得传参发生异常。


解决方法

1、 值传递

直接传 i 的值进入,而不是传地址。

...void* thread_worker(void*  arg)
{
    int  thread_num = (int)arg;    ...}

int main(int argc,char* argv[])
{    ...
    for(i=0;iNULL,thread_worker,(void*)i);
    }    ...
    return 0;
}
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

现在传参正常了。
但是,也许你还是有点不满意,编译的时候有个警告,不太想看见它:

因为编译器虽然支持(void )转化为(int),但还是不推荐这样做,所以产生了 warning 。我们还可以用其他方法。*

2、 借用数组传参

将3次传递的参数分别保存为数组的不同元素:

//关键代码
void* thread_worker(void*  arg)
{
    //先将void* 转为 int* 再赋值
    int  thread_num = *(int*)arg;    ...}

int main(int argc,char* argv[])
{
    int                 i,rtn,ch;
    pthread_t           pthread_id[MAX_THREAD] =  {0};  //存放线程
    //保存参数的数组
    int                 param[3];    for(i=0;iNULL,thread_worker,param+i);
    }    ...

    return 0;
}
  • 1

  • 2

  • 3

  • 4

  • 5

  • 6

  • 7

  • 8

  • 9

  • 10

  • 11

  • 12

  • 13

  • 14

  • 15

  • 16

  • 17

  • 18

  • 19

  • 20

  • 21

  • 22

  • 23

  • 24

这个方法可以解决问题,但是不具有灵活性,如果创建了很多个线程呢,难道要有一个很大的数组么。线程数量不确定怎么办,数组定为多大才是合适的呢?
下面我们采用第3种方法。

3、动态申请临时内存

因为每次申请内存返回的地址都不一样,所以参数传指针进去不会有问题,要记得赋值完释放内存,避免内存泄漏。

...void* thread_worker(void*  arg)
{
    //先将void* 转为 int* 再赋值
    int  thread_num = *(int*)arg;
    //释放内存
    free((int*)arg);    ...}

int main(int argc,char* argv[])
{
    int                 i,rtn,ch;
    pthread_t           pthread_id[MAX_THREAD] =  {0};  //存放线程

    int                 *param;    for(i=0;iNULL,thread_worker,param);
    }    ...






请到「今天看啥」查看全文