文章目录
- 1. Linux线程概念
- 1.1 线程和进程
- 1.2 虚拟地址是如何转换到物理地址的
- 1.3 线程的优点
- 1.4 线程的缺点
- 1.5 线程异常
- 1.6 线程用途
- 2. Linux进程VS线程
- 2.1 进程和线程
- 2.2 关于进程线程的问题
- 3. Linux线程控制
- 3.1 POSIX线程库
- 3.2 创建线程
- 3.3 线程终止
- 3.4 线程等待
- 3.5 分离线程
- 4. 线程ID及进程地址空间布局
1. Linux线程概念
1.1 线程和进程
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
一切进程至少都有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
线程是进程内的一个执行分支。线程的执行粒度要比进程要细
线程在进程的地址空间内运行。
为什么?
在Linux中,线程在进程“内部”执行,因为任何执行流要执行,都要有资源。地址空间是进程的资源窗口
在Linux中,线程的执行粒度要比进程要更细,因为线程执行进程代码的一部分
如何理解我们以前的进程?
操作系统以进程为单位,给我们分配资源,我们当前的进程内部,只有一个执行流。
重新定义线程和进程:
什么叫做线程?我们认为,线程操作系统调度的基本单位。
重新理解进程,内核观点:进程是承担分配系统资源的基本实体。
线程是我进程内部的执行流资源,一个进程有多个线程。
执行流也是资源。
Windows设计者是专门为了线程设计了一套和进程差不多的结构体
struct tcb
,用来管理线程。不过,Linux设计觉得,进程和线程差不多,只是线程的颗粒度小一点,直接套用进程的那一套不就行了吗?
所以,Linux有线程,但是没有真正意义上的线程。
而是用“进程”(内核数据结构)模拟的线程。(这是一个卓越的想法)
Linux中的执行流也叫做:轻量级进程
线程 <= 执行流 <= 进程
1.2 虚拟地址是如何转换到物理地址的
1.3 线程的优点
创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
1.4 线程的缺点
性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多
1.5 线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
1.6 线程用途
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
2. Linux进程VS线程
2.1 进程和线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器
栈
errno
信号屏蔽字
调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
进程和线程的关系如下图:
2.2 关于进程线程的问题
如何看待之前学习的单进程?
就是具有一个线程执行流的进程。
3. Linux线程控制
3.1 POSIX线程库
pthread
是 “POSIX threads
” 的简写。POSIX
线程库是官方完整名称,pthread
就是它的常用简称。- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“
pthread_
”打头的- 要使用这些函数库,要通过引入头文
<pthread.h>
- 链接这些线程函数库时要使用编译器命令的“
-lpthread
”选项
Linux
内核本身并不直接支持线程这个概念,它只认识一种叫"轻量级进程"的东西。这就像内核只提供了基础的零件,而不是完整的工具。它不会给我直接提供线程的系统调用,只会给我们提供轻量级进程的系统调用。但是我们日常编程时需要用到线程,这时候就需要一个"翻译官"-
pthread
线程库来帮忙。这个库就像是一个包装器,它把我们需要的线程操作转换成内核能理解的轻量级进程操作。好消息是,几乎所有的
Linux
系统都默认安装了这个pthread
库。所以当我们在Linux下要写多线程程序时,需要调用这个pthread库提供的接口,而不是直接使用内核的接口。
pthread
线程库 – 应用层 – 轻量级进程接口进行封装。为用户提供直接线程的接口
Linux
中编写多线程代码需要使用第三方pthread
库。
3.2 创建线程
功能:创建一个新的线程
原型int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数thread:返回线程IDattr:设置线程的属性,attr为NULL表示使用默认属性start_routine:是个函数地址,线程启动后要执行的函数arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
- 传统的一些函数是,成功返回
0
,失败返回-1
,并且对全局变量errno
赋值以指示错误。pthreads
函数出错时不会设置全局变量errno
(而大部分其他POSIX
函数会这样做)。而是将错误代码通过返回值返回pthreads
同样也提供了线程内的errno
变量,以支持其它使用errno
的代码。对于pthreads
函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno
变量的开销更小
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>// 线程执行函数
void *rout(void *arg) {int i;// 无限循环for( ; ; ) {printf("I'am thread 1\n"); // 打印线程1的标识信息sleep(1); // 休眠1秒,降低CPU占用}
}int main( void )
{pthread_t tid; // 用于保存线程IDint ret; // 保存pthread_create的返回值// 创建新线程// pthread_create参数:// 1. &tid: 存储新创建线程ID的地址// 2. NULL: 线程属性,NULL表示使用默认属性// 3. rout: 线程将要执行的函数// 4. NULL: 传递给线程函数的参数,这里没有参数传递if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {// 如果线程创建失败:// strerror将错误码转换为错误信息字符串fprintf(stderr, "pthread_create : %s\n", strerror(ret));// 退出程序,返回失败状态exit(EXIT_FAILURE);}int i;// 主线程的无限循环for(; ; ) {printf("I'am main thread\n"); // 打印主线程的标识信息sleep(1); // 休眠1秒,降低CPU占用}
}
运行结果:
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson36$ ./program
I'am main thread
I'am thread 1
I'am main thread
I'am thread 1
I'am main thread
I'am thread 1
I'am main thread
I'am thread 1
^C
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson36$
#include <pthread.h>
// 获取线程ID
pthread_t pthread_self(void);
打印出来的
tid
是通过pthread
库中有函数pthread_self
得到的,它返回一个pthread_t
类型的变量,指代的是调用pthread_self
函数的线程的 “ID
”。怎么理解这个“
ID
”呢?这个“ID
”是pthread
库给每个线程定义的进程内唯一标识,是pthread
库维持的。由于每个进程有自己独立的内存空间,故此“
ID
”的作用域是进程级而非系统级(内核不认识)。其实
pthread
库也是通过内核提供的系统调用(例如clone
)来创建线程的,而内核会为每个线程创建系统全局唯一的“ID
”来唯一标识这个线程。
使用PS
命令查看线程信息
运行代码后执行:
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson36$ ps -aL | head -1 && ps -aL | grep programPID LWP TTY TIME CMD302889 302889 pts/1 00:00:00 program302889 302890 pts/1 00:00:00 program
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson36$
LWP
是什么呢?LWP
得到的是真正的线程ID
。之前使用pthread_self
得到的这个数实际上是一个地址,在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID
,线程栈,寄存器等属性。在
ps -aL
得到的线程ID
,有一个线程ID
和进程ID
相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread
系列函数都是pthread
库提供给我们的。而pthread
库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。
3.3 线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数
return
。这种方法对主线程不适用,从main
函数return
相当于调用exit
。- 线程可以调用
pthread_exit
终止自己。- 一个线程可以调用
pthread_cancel
终止同一进程中的另一个线程。
pthread_exit
函数
功能:线程终止
原型:void pthread_exit(void *value_ptr);
参数:value_ptr:void *value_ptr 是线程的返回值,value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit
或者return
返回的指针所指向的内存单元必须是全局的或者是用malloc
分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel
函数
功能:取消一个执行中的线程
原型:int pthread_cancel(pthread_t thread);
参数:thread:线程ID
返回值:成功返回0;失败返回错误码
3.4 线程等待
为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
功能:等待线程结束
原型int pthread_join(pthread_t thread, void **value_ptr);
参数:thread:线程IDvalue_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到
id
为thread
的线程终止。thread
线程以不同的方法终止,通过pthread_join
得到的终止状态是不同的,总结如下:
- 如果
thread
线程通过return
返回,value_ptr
所指向的单元里存放的是thread
线程函数的返回值。- 如果
thread
线程被别的线程调用pthread_cancel
异常终掉,value_ptr
所指向的单元里存放的是常数PTHREAD_CANCELED
。- 如果
thread
线程是自己调用pthread_exit
终止的,value_ptr
所指向的单元存放的是传给pthread_exit
的参数。- 如果对
thread
线程的终止状态不感兴趣,可以传NULL
给value_ptr
参数。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>// 线程1函数 - 通过return返回值
void *thread1( void *arg )
{printf("thread 1 returning ... \n");int *p = (int*)malloc(sizeof(int)); // 分配内存保存返回值*p = 1; // 设置返回值为1return (void*)p; // 返回指针
}// 线程2函数 - 通过pthread_exit返回值
void *thread2( void *arg )
{printf("thread 2 exiting ...\n");int *p = (int*)malloc(sizeof(int)); // 分配内存保存返回值*p = 2; // 设置返回值为2pthread_exit((void*)p); // 使用pthread_exit退出并返回指针
}// 线程3函数 - 无限循环直到被其他线程取消
void *thread3( void *arg )
{while ( 1 ){ // 无限循环printf("thread 3 is running ...\n");sleep(1); // 休眠1秒}return NULL;
}int main( void )
{pthread_t tid; // 线程IDvoid *ret; // 用于存储线程返回值的指针// 创建并等待线程1完成pthread_create(&tid, NULL, thread1, NULL); // 创建线程1pthread_join(tid, &ret); // 等待线程1结束并获取返回值printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);free(ret); // 释放返回值的内存// 创建并等待线程2完成pthread_create(&tid, NULL, thread2, NULL); // 创建线程2pthread_join(tid, &ret); // 等待线程2结束并获取返回值printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);free(ret); // 释放返回值的内存// 创建线程3并在3秒后取消它pthread_create(&tid, NULL, thread3, NULL); // 创建线程3sleep(3); // 主线程休眠3秒pthread_cancel(tid); // 取消线程3pthread_join(tid, &ret); // 等待线程3结束并获取返回状态if ( ret == PTHREAD_CANCELED )printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n", tid);elseprintf("thread return, thread id %X, return code:NULL\n", tid);
}
运行结果:
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson36$ ./program
thread 1 returning ...
thread return, thread id 15C2A700, return code:1
thread 2 exiting ...
thread return, thread id 15C2A700, return code:2
thread 3 is running ...
thread 3 is running ...
thread 3 is running ...
thread return, thread id 15C2A700, return code:PTHREAD_CANCELED
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson36$
3.5 分离线程
- 默认情况下,新创建的线程是
joinable
的,线程退出后,需要对其进行pthread_join
操作,否则无法释放资源,从而造成系统泄漏。- 如果不关心线程的返回值,
join
是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
参数:pthread_t threadthread是要分离的线程IDpthread_t是线程ID的类型
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
// 两种使用方式
pthread_t tid;// 1. 分离其他线程
pthread_create(&tid, NULL, thread_func, NULL);
pthread_detach(tid);// 2. 线程分离自己
void* thread_func(void* arg) {pthread_detach(pthread_self()); // pthread_self()获取当前线程IDreturn NULL;
}
joinable
和分离是冲突的,一个线程不能既是joinable
又是分离的。
// joinable状态(默认)
pthread_t tid1;
pthread_create(&tid1, NULL, func, NULL);
// - 必须被其他线程join
// - 资源需要手动回收
// - 可以获取线程返回值// detached状态
pthread_t tid2;
pthread_create(&tid2, NULL, func, NULL);
pthread_detach(tid2);
// - 结束时自动回收资源
// - 不能被join
// - 无法获取返回值
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>// 线程执行函数
void *thread_run( void * arg )
{// pthread_detach用于分离线程,使线程结束时自动回收资源// pthread_self()获取当前线程的线程IDpthread_detach(pthread_self()); // 打印传入的参数字符串printf("%s\n", (char*)arg);return NULL;
}int main( void )
{pthread_t tid; // 用于保存线程ID// pthread_create创建新线程// 参数1:指向线程ID的指针// 参数2:线程属性,NULL表示使用默认属性// 参数3:线程执行的函数// 参数4:传递给线程函数的参数if ( pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0 ) {printf("create thread error\n");return 1;}int ret = 0;// sleep(1)很重要// 这里等待1秒是为了让新创建的线程有机会执行pthread_detach()// 如果主线程不等待,可能会在线程detach之前就执行pthread_joinsleep(1);// pthread_join尝试等待tid指定的线程结束// 因为线程已经分离(detach),所以pthread_join会失败// 参数1:要等待的线程ID// 参数2:用于存储线程返回值的指针,这里为NULL表示不关心返回值if ( pthread_join(tid, NULL ) == 0 ) {printf("pthread wait success\n");ret = 0;} else {// 因为线程已分离,所以这里会执行,打印等待失败printf("pthread wait failed\n");ret = 1;}return ret;
}
4. 线程ID及进程地址空间布局
pthread_create
函数会产生一个线程ID
,存放在第一个参数指向的地址中。该线程ID
和前面说的线程ID
不是一回事。- 前面讲的线程
ID
属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。pthread_create
函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID
,属于NPTL
线程库的范畴。线程库的后续操作,就是根据该线程ID
来操作线程的。- 线程库
NPTL
提供了pthread_self
函数,可以获得线程自身的ID
:
pthread_t pthread_self(void);
返回值:pthread_t返回调用线程的线程IDpthread_t是线程标识符类型
参数:void无参数只能获取当前调用线程的ID
pthread_t
到底是什么类型呢?取决于实现。对于Linux
目前实现的NPTL
实现而言,pthread_t
类型的线程ID
,本质就是一个进程地址空间上的一个地址。