这里说一下相关的基础知识:
线程概念
什么是线程
LWP:light weight process 轻量级的进程,本质仍是进程(在Linux环境下)
进程:独立地址空间,拥有PCB
线程:也有PCB,但没有独立的地址空间(共享)
区别:在于是否共享地址空间。 独居(进程);合租(线程)。
Linux下: 线程:最小的执行单位
进程:最小分配资源单位,可看成是只有一个线程的进程。
Linux内核线程实现原理
类Unix系统中,早期是没有"线程"概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。
1. 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
2. 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
3. 进程可以蜕变成线程
4. 线程可看做寄存器和栈的集合
5. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位
察看LWP号:ps –Lf pid 查看指定线程的lwp号。
三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元
参考:《Linux内核源代码情景分析》 ----毛德操
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。
如果复制对方的地址空间,那么就产出一个"进程";如果共享对方的地址空间,就产生一个"线程"。
因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
线程共享资源
1.文件描述符表
2.每种信号的处理方式
3.当前工作目录
4.用户ID和组ID
5.内存地址空间 (.text/.data/.bss/heap/共享库)
线程非共享资源
1.线程id
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)
4.errno变量
5.信号屏蔽字
6.调度优先级
线程优、缺点
优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便
缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线
程差别不是很大。
线程控制原语
pthread_self函数
获取线程ID。其作用对应进程中 getpid() 函数。
头文件:#include <pthread.h>
pthread_t pthread_self(void); 返回值:成功:0; 失败:无!
pthread_t:当前Linux中可理解为:typedef unsigned long int pthread_t;//无符号长整形
线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现
线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)
注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。
pthread_create函数
创建一个新线程。 其作用,对应进程中fork() 函数。
头文件:#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值:成功:0; 失败:错误号 -----Linux环境下,所有线程特点,失败均直接返回错误号。
参数:
pthread_t:当前Linux中可理解为:typedef unsigned long int pthread_t;//无符号长整形
参数1:传出参数,保存系统为我们分配好的线程ID
参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。
参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。参数是函数指针,只能传递函数名,不能传递参数。所以就是只能有一个参数。
参数4:线程主函数执行期间所使用的参数。
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态,稍后详细介绍pthread_join。
pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid(2)可以获得当前进程的id,是一个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self(3)可以获得当前线程的id。
attr参数表示线程属性,本节不深入讨论线程属性,所有代码例子都传NULL给attr参数,表示线程属性取缺省值,感兴趣的读者可以参考APUE。
现在我们先预热:创建一个新线程,打印线程ID。注意:链接线程库 -lpthread。
#include
<stdio.h>
#include
<pthread.h>
#include
<unistd.h>
void *tfn(void *arg)
{
printf("我是线程,我的ID = %lu\n", pthread_self());
return
NULL;
}
int main(void)
{
pthread_t tid;
pthread_create(&tid, NULL, tfn, NULL);
sleep(1);
printf("我是进程,我的进程ID = %d\n", getpid());
return 0;
}
结果:
由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印。如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,下一节我们会看到更好的办法。要这样写命令:gcc -pthread pthread_create.c -o pthread_create
现在进入主题:循环创建多个线程,每个线程打印自己是第几个被创建的线程。(类似于进程循环创建子进程)
#include
<pthread.h>
#include
<stdio.h>
#include
<unistd.h>
#include
<stdlib.h>
void *tfn(void *arg)
{
int i;
i = (int)arg;
sleep(i); //通过i来区别每个线程
printf("我是第%d个线程,我的线程ID = %lu\n", i + 1, pthread_self());
return
NULL;
}
int main(int
argc, char *argv[])
{
int n = 5, i;
pthread_t tid;
if (argc == 2)
n = atoi(argv[1]);
for (i = 0; i < n; i++) {
pthread_create(&tid, NULL, tfn, (void *)i);
//将i转换为指针,在tfn中再强转回整形。
}
sleep(n);
printf("我是main函数,但是我不是进程,我的ID = %lu\n", pthread_self());
return 0;
}
结果:
一切正常,现在我解释一些代码: pthread_create(&tid, NULL, tfn, (void *)i);这里的 (void *)i参数应该是指针,但是我们这里是将其强转为void*类型了,并且编译过程中也给我警告了:
位机,如果是在32位机上编译是没有这样的错误的。这个警告是在说int和void转化中的长度不一致(在我的机器上)。void在64位机上是8位,int一般来说都是4位。这在第一次转化的时候是小变大,会发生补零,在高位上补零;第二次在i = (int)arg;这里发生大变小转化,会截取,截取高位。所以,实际上对于这个程序来说是没有影响的。所以那两个警告是没有问题的。其他的我相信是没有什么问题的。
修改为(void *)&i, 将线程主函数(tfn)内改为 i=*((int *)arg) 是否可以?
开始了,一会儿4个线程,一会儿5个线程。这很蛋疼啊:第四个参数应该是指针啊,没错啊。可就是不对。其实也很好理解的,线程之间共享一个用户空间,我们这样传递的是i的地址过去,然后在运行线程主函数的时候依据地址找i的值,那么,问题出现了,cpu是个很快的男人,从main到线程主函数这之间有时间差吧?所以,在那么点时间内,i的值发生改变了。那为什么有时候线程个数不足?上面只要main一结束,管你后面是不是还有线程的,统统杀死。