Linux系统编程——线程
1. 线程
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。
1.1 什么是线程
进程A独享地址空间,线程B,C共享进程地址空间
1.2 Linux内核线程实现原理
查看LWP号:ps –Lf pid
查看指定线程的lwp号。
线程号:cpu分配时间轮片的依据
线程ID:进程内部区分线程依据
三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元
三级映射描述了MMU如何把虚拟地址映射到物理内存
参考:《Linux内核源代码情景分析》 ----毛德操
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。
因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
1.3 线程共享资源与非共享资源
线程共享资源 | 线程非共享资源 |
---|---|
文件描述符表 | 线程id |
每种信号的处理方式 | 处理器现场(寄存器的值)和栈指针(内核栈) |
当前工作目录 | 独立的栈空间(用户空间栈) |
用户ID和组ID | errno变量 |
内存地址空间 (.text/.data/.bss/heap/共享库)(stack不共享) | 信号屏蔽字 |
- | 调度优先级 |
1.4 线程优、缺点
- 优点
- 提高程序并发性
- 开销小
- 数据通信、共享数据方便
- 缺点:
- 库函数,不稳定
- 调试、编写困难、gdb不支持
- 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。
2. 线程控制原语(重点)
2.1 pthread_self函数
获取线程ID。其作用对应进程中 getpid() 函数。
pthread_t pthread_self(void);
返回值:成功:0; 失败:无!- 线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现
- 线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)
- 注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。
2.2 pthread_create函数
创建一个新线程。 其作用,对应进程中fork() 函数。
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:传出参数(没有用const描述),保存系统为我们分配好的线程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 <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
using namespace std;
//线程函数,线程创建成功,系统自动调用
void *thrd_func(void *arg){
cout << "In thread: thread id is :" << pthread_self() << " pid is :" << getpid() << endl;
return NULL;
}
int main(){
pthread_t tid;
cout << "In main 1: thread id is :" << pthread_self() << " pid is :" << getpid() << endl;
int ret = pthread_create(&tid,NULL,thrd_func,NULL);
if(ret != 0){
perror("pthread_create error");
exit(1);
}
sleep(1);//主线程等待1s,保证子线程输出
cout << "In main 2: thread id is :" << pthread_self() << " pid is :" << getpid() << endl;
return 0;//将当前进程退出
}
由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印。如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,下一节我们会看到更好的办法。
循环创建多个线程,每个线程打印自己是第几个被创建的线程。(类似于进程循环创建子进程)
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *thrd_func(void *arg){
long i = (long)arg;
sleep(i);//保证子线程按顺序输出
cout << "I am " << i+1 << " thread: thread id is :" << pthread_self() << " pid is :" << getpid() << endl;
return NULL;
}
int main(){
pthread_t tid;
int i;
//循环创建n个子线程
for(i = 0