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

来源:互联网 发布:软件总工程师简历 编辑:程序博客网 时间:2024/05/17 08:59

原文链接: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<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<unistd.h>#include<ctype.h>#include<pthread.h>#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<MAX_THREAD;i++)    {//传 &i         pthread_create(&pthread_id[i],NULL,thread_worker,&i);    }    do    {        unsigned long long   sum = 0;        for(i=0;i<MAX_THREAD;i++)        {            sum += counter[i];            printf("No.%d: %llu\n",i,counter[i]);        }        printf("%llu/%llu\n",main_counter,sum);    }while((ch = getchar())!='q');    return 0;}

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

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


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

问题1传参很诡异

原因:传参被主线程破坏

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

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

这时发现运行结果是这样的:
这里写图片描述

很奇怪,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;i<MAX_THREAD;i++)    {         pthread_create(&pthread_id[i],NULL,thread_worker,(void*)i);    }    ...    return 0;}

这里写图片描述

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

这里写图片描述

因为编译器虽然支持(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;i<MAX_THREAD;i++)    {         param[i] = i;        pthread_create(&pthread_id[i],NULL,thread_worker,param+i);    }    ...    return 0;}

这个方法可以解决问题,但是不具有灵活性,如果创建了很多个线程呢,难道要有一个很大的数组么。线程数量不确定怎么办,数组定为多大才是合适的呢?
下面我们采用第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;i<MAX_THREAD;i++)    {         //申请内存临时保存参数        param = (int*)malloc(sizeof(int));        *param = i;        pthread_create(&pthread_id[i],NULL,thread_worker,param);    }    ...    return 0;}

这回满意了。

问题2main_counter < sum

原因:子线程相互竞争 main_counter ,导致不能正常执行 加 1 操作。

分析:
  解决了问题1,我们发现还有问题,main_counter 居然不等于 sum 的值,与理论不符。这主要是由于 main_counter++ 语句并不是原子操作。
什么是原子操作呢?
   通俗的将,就是线程执行某个操作的时候不能被其他线程打断或破坏。好比说,你去食堂吃饭,刚刷了卡,结果给你的菜却被另一个刚来的同学给端走了。

简单的看一下执行 main_counter++ 的过程:
这里写图片描述

可能当线程 1 还没完成加 1 操作的时候,此时,线程2 也开始执行 main_counter++(如果是单核CPU,线程 1 会暂时保存寄存器中的值,待下一个时间片到来时,恢复现场继续操作; 如果是多核CPU,线程 1 和线程 2 可能会同时执行++) ,但是线程 2 看到的main_counter 还是 0 ,所以线程 2完成了加 1 操作后,main_counter 还是 1。虽然两个线程各执行了一次加 1 操作,但是最终 main_counter 实际上只加了1次。这就导致main_counter 比理论值偏小。在3个线程运行的情况下,理论值最大是实际值的3倍。


解决办法:加锁

代码2:

//初始化锁static pthread_mutex_t    mutex = PTHREAD_MUTEX_INITIALIZER;unsigned long long   main_counter,counter[MAX_THREAD]={0};void* thread_worker(void*  arg){    //先将void* 转为 int* 再赋值    int  thread_num = *(int*)arg;    //释放内存    free((int*)arg);    for(;;)    {        //加锁        pthread_mutex_lock(&mutex);        counter[thread_num]++;    //本线程的counter 加 1        main_counter++;        //解锁        pthread_mutex_unlock(&mutex);    }}int main(int argc,char* argv[]){    int                 i,rtn,ch;    pthread_t           pthread_id[MAX_THREAD] =  {0};  //存放线程    int                 *param;    for(i=0;i<MAX_THREAD;i++)    {         //申请内存临时保存参数        param = (int*)malloc(sizeof(int));        *param = i;        pthread_create(&pthread_id[i],NULL,thread_worker,param);    }    do    {        unsigned long long   sum = 0;        for(i=0;i<MAX_THREAD;i++)        {            sum += counter[i];            printf("No.%d: %llu\n",i,counter[i]);        }        printf("%llu/%llu\n",main_counter,sum);    }while((ch = getchar())!='q');    //销毁锁资源    pthread_mutex_destroy(&mutex);    return 0;}

这里写图片描述

看吧,sum 不再比 main_counter 大了。

但是,还是不能消停,这回发现 main_counter 居然比 sum 大了。如果是单核CPU,比如在单核云服务器、虚拟机(VMWare、VirtualBox等)系统上运行时,可能看到main_counter = sum ,但是不代表不会出现main_counter > sum 的情况,还是有隐患的。这些问题我们接下来继续解决。


问题3main_counter 为毛比 sum 大了?

原因:还是竞争。调整加锁的位置。

   其实这是主线程输出的问题,问题主要出在以下这段代码:
这里写图片描述

第57~61 行是在计算counter[i] 的和,将计算结果保存到sum 中,然后第62行输出结果。问题是在这个过程中也会发生竞争:
(1)如果在主线程刚执行完求和操作还未输出时,时间片用完了,CPU被子线程抢占,执行了main_counter++,但sum 不会再同步增加了,所以最后输出时,main_counter > sum 。
(2)如果是多核CPU,在主线程完成求和操作到输出结果这一段时间内很可能有子线程在并行执行main_counter++,同样sum 却不会增加,导致输出时,main_counter > sum 。


那在单核系统上为什么代码2 执行几乎是正常的呢?
   在单核系统上,运行出现异常是原因(1)导致,因为单核上CPU调度线程是用的时间片轮转。大概因为57~62 行这几条语句执行时间太短了,一个时间片内可以执行完,几乎不会在中途被打断。


   我们可以在62行前加 sleep 睡眠,这时,隐患就暴露出来了。
这里写图片描述

然后在我的单核云服务器上运行:
这里写图片描述

果然,main_counter 比 sum 大了。


解决办法:调整锁的位置

   解决问题的关键就是 a、要让 main_counter++ 和 counter[i]++ 是保持同步更新的, 这两条语句中间不能被打断; b、求和操作完成后,不能再让 main_counter 增加。


我们把解锁操作移到 62 行之后就可以了。

最终的完整代码如下:

#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<unistd.h>#include<ctype.h>#include<pthread.h>#define MAX_THREAD  3  //线程个数//初始化锁static pthread_mutex_t    mutex = PTHREAD_MUTEX_INITIALIZER;unsigned long long   main_counter,counter[MAX_THREAD]={0};void* thread_worker(void*  arg){    //先将void* 转为 int* 再赋值    int  thread_num = *(int*)arg;    //释放内存    free((int*)arg);    for(;;)    {        //加锁        pthread_mutex_lock(&mutex);        counter[thread_num]++;    //本线程的counter 加 1        main_counter++;    }}int main(int argc,char* argv[]){    int                 i,rtn,ch;    pthread_t           pthread_id[MAX_THREAD] =  {0};  //存放线程    int                 *param;    for(i=0;i<MAX_THREAD;i++)    {         //申请内存临时保存参数        param = (int*)malloc(sizeof(int));        *param = i;        pthread_create(&pthread_id[i],NULL,thread_worker,param);    }    do    {        unsigned long long   sum = 0;        for(i=0;i<MAX_THREAD;i++)        {            sum += counter[i];            printf("No.%d: %llu\n",i,counter[i]);        }        printf("%llu/%llu\n",main_counter,sum);        //解锁        pthread_mutex_unlock(&mutex);    }while((ch = getchar())!='q');    //销毁锁资源    pthread_mutex_destroy(&mutex);    return 0;}

运行: main_counter == sum ,与理论符合
这里写图片描述

  解决了所有问题后,线程运行的效率远远的变慢了,这是因为锁的存在导致一个线程在运行时,其他线程几乎是在阻塞的。但是大可不必在意这个,因为这个程序只是用来帮助更好的理解线程资源共享、线程并行、线程抢占和线程调度的,实际应用中,肯定不会把一个求和操作搞的这么错综复杂的。

   如有问题或错误,欢迎指出。

4 0
原创粉丝点击