线程理解
线程就是一个执行流。
线程在进程内部运行 ----- 本质:线程在进程的地址空间内运行
//Linux中的每个执行流(线程)也被叫做“轻量级进程”。
线程与进程
我们刚才说了,线程是在进程内部运行的,那么进程到底是什么?这里我们需要给进程一个明确的定义了!
在内核角度:进程是承担分配系统资源的基本实体叫做进程。而线程则是调度的基本单位。
//进程包括(task_struct、地址空间、页表)
而对于线程而言,线程只是进程中的一个task_struct,一个进程中可以有多个线程。对于Linux而言,在Linux中并不存在真正意义的线程,因为Linux中没有为线程创建相应的数据结构,而是复用了进程的结构体。站在用户的角度,想要创建线程的话,可以使用原生线程库去创建线程。
根据这张图,对于CPU而言,CPU有办法识别task_struct是线程的还是进程的吗?
不能识别,也不需要识别!CPU只关心一个一个的执行流(task_struct)。
线程的优缺点
· 优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 线程切换的代价要比进程切换小得多
- 线程占用的资源要比进程少得多
- 线程能充分利用多处理器的可并行数量
- 在等待慢速IO操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- IO密集型应用,为了提高性能,将IO操作重叠,线程可以同时等待不同的IO操作
计算密集型:执行流的大部分任务,主要以计算为主:加密解密、排序查找
I/O密集型:执行流的大部分任务,主要以IO为主:刷新磁盘、访问网络、访问数据库
· 缺点
- 性能损失:指当线程数量比可用的处理器数量多时,会花费额外的开销在线程的同步与调度上。
- 鲁棒性低:在一个多线程程序当中,当一个线程出现异常崩溃时会导致整个进程都崩溃。
- 缺乏访问控制
- 编程难度提高
线程的私有数据与公有数据
线程共享进程的数据,但是线程也有一部分私有的数据:
- 线程ID
- 寄存器数据(保存了一个线程的上下文信息,以便该现成在被重新切换到时能够恢复当前的数据)
- 栈
- errno
- 信号屏蔽字
- 调度优先级
线程的公有数据:
- 代码段
- 数据段
- 堆区
- 文件描述符表
- 每种信号的处理方式(SIG_DFL、SIG_IGN、自定义函数)
- 当前工作目录
- 用户ID和组ID
线程创建
注意:使用pthread线程库的时候,编译时要携带手动链接pthread库。即带上 -lpthread。
int pthread_create(pthread_t* thread, const pthread_attr_t* attr,
void* (*start_routine) (void*), void* arg);
参数:
thread: 输出型参数,用来获取线程ID的。 #传入的时候要取地址哦!
attr: 设置线程的属性信息,attr为NULL代表使用默认属性。
start_routine: 函数地址。这里使用回调函数的方法,告诉新启动的线程要执行哪个函数。
arg: 传给线程启动函数的参数,也就是配合start_routine函数的参数使用。
返回值:
成功返回0;失败返回错误码errno
线程的查看
查看进程内的多个轻量级进程的指令:
ps -aL
同一个进程中的多个轻量级进程的PID都是一样的,他们的区别在于LWP。
LWP (Light Weight Process): 轻量级进程ID
再纠正一个概念:
OS调度的时候,是靠LWP,而不是PID!//因为OS调度的基本单位是线程。
线程ID及获取
pthread_t pthread_self(void);
功能:返回当前线程的线程ID。
线程ID的类型是pthread_t,它实际上是unsigned long类型的数据。
问题:线程ID的含义是什么?
我们将线程ID以 %p 的形式打印出来。
线程ID的本质其实是地址!它是指向动态库中的某一块空间的起始地址!这里的"某一块空间"是用来描述线程相关信息的数据块,实际是使用pthread库中的函数创建线程,也就是pthread库帮我们对线程进行管理,而库的本质也是文件,库在运行的时候会被加载到堆栈之间的共享区,因此就有了下图所描绘的样子。
线程ID与LWP
我们刚才在ps -aL当中获取到了一个叫做LWP的东西,这个是轻量级进程ID,而我们在创建线程时获取到了线程ID,这两个ID都能够唯一的标识一个线程,那它们之间是什么关系呢?
我们在ps -aL获取到的LWP的值和线程ID是什么关系呢?
线程ID ≠ LWP
但是它们的关系是 1 :1的关系。
// 线程ID是用户级线程的标识,LWP是内核的轻量级进程ID。
线程终止
终止线程的方法:
①:在线程函数中直接使用return。 #对于主线程的return,会直接终止整个进程。
②:在线程函数中使用pthread_exit()来终止自己。
③:在其他线程中可以调用pthread_cancel来指定终止同一进程中的特定线程。
注意:
在任意线程中使用exit函数,都会终止整个进程的!!!因此我们一般不在线程中使用exit函数。
· pthread_exit函数
void pthread_exit(void* retval);
功能:让当前线程终止
参数:
retval: 存放线程的退出码。
· pthread_cancel函数
int pthread_cancel(pthread_t thread);
功能:让指定线程ID的线程终止
参数:
thread: 指定取消的线程ID
返回值:
成功返回0;失败返回错误码
注意:
- 一般用于在主线程中去取消其他线程。
- 不推荐新线程去取消主线程,在不同的版本下可能会有不一样的结果。
- 也可以用于自己取消自己,但是这与pthread_cancel放置在代码中的位置也有关系。不推荐。
线程等待
问题:为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间当中。 #会造成内存泄漏!
- 创建新的线程也不会复用刚才退出的线程的地址空间。
#这块跟僵尸进程很类似,但是不存在僵尸线程这一说法!,所以线程是需要被等待的!
· pthread_join函数
int pthread_join(pthread_t thread, void** retval);
参数:
thread: 线程ID
retval: 它指向一个指针,指针所指向的是所等待线程的返回值。不关心的话可以设置为NULL。
对于retavl的说明
- 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值
- 如果thread线程被背的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED
- 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元里存放的是传给pthread_exit的参数
- 如果对thread线程的终止状态不感兴趣,可以将retval设置为NULL
返回值:
成功返回0;失败返回错误码
演示
#include <iostream>
#include <pthread.h>
using namespace std;
void* Routine(void* arg)
{
cout << (char*)arg << endl;
pthread_exit((void*)1);
}
int main()
{
pthread_t tid;
const char* msg = "A message!";
pthread_create(&tid, NULL, Routine, (void*)msg);
pthread_join(tid, NULL);
return 0;
}
线程分离
线程分离的作用就是让线程自己退出时,自动释放线程资源。 //这样就不需要主线程等它了
· pthread_detach函数
int pthread_detach(pthread_t thread);
thread是线程ID。
可以让线程组内的其他线程对目标线程进行分离,也可以是线程自己分离自己。 #传pthread_self就行
演示
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void* Routine(void* arg)
{
pthread_detach(pthread_self()); //线程分离后就不用主线程再join子线程了
cout << (char*)arg << endl;
pthread_exit((void*)1);
}
int main()
{
pthread_t tid;
const char* msg = "A message!";
pthread_create(&tid, NULL, Routine, (void*)msg);
sleep(1);
//如果线程已经分离了,那么pthread_join函数就会join失败,返回值就不是0而是错误码
if(pthread_join(tid, NULL) == 0)
{
cout << "join thread Success!" << endl;
}
else
{
cout << "join thread Failed!" << endl;
}
return 0;
}