原文链接: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 thread_num = *(int*)arg;
for(;;)
{
counter[thread_num]++;
main_counter++;
}
}int main(int argc,char* argv[])
{ int i,rtn,ch;
pthread_t pthread_id[MAX_THREAD] = {0};
for(i=0;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);
这时发现运行结果是这样的:
很奇怪,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);
} ...