前言
目录 1.Linux下的线程概念 2.Linux线程控制:pthread线程库 在单执行流的进程中,此执行流独占了进程的所有资源
在一个进程内部,有时不一定只有一个执行流,在多执行流下,多个执行流共享了进程的地址空间,我们把“一个程序内部的控制序列”叫做线程
线程本质是在进程的地址空间内运行 进程的切换涉及到页表映射的切换,而线程的切换只是切换了指令序列而在同一个地址空间中进行
那么我们给出下面两个重要概念
- 进程是操作系统分配资源的基本实体
- 线程是进程内部的一个执行流
举个栗子: 在现实生活中,假如我们把社会资源的基本单位看作是家庭 ,比如我们经常以家庭年收入统计社会财富的分配状况,那么此时:
家庭成员共享了家庭的资源,家庭成员之间有共享的资源,也有私人的小秘密。
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
线程可以被创建、等待、终止、控制 家庭有生老病死、家规等等…
1.Linux下的线程概念
在Linux下,其实没有真正意义上的线程概念,是用进程来模拟的
Linux的进程叫做轻量级进程
LWP是轻量级进程,在Linux下进程是资源分配的基本单位 ,线程是cpu调度的基本单位 ,而线程使用进程pcb描述实现,操作系统在创建线程时给每个线程都创建一个pcb结构体,并且同一个进程中的所有pcb共用同一个虚拟地址空间,因此相较于传统进程更加的轻量化有了更多执行流之后。进程变成了分配资源的基本实体,进程一旦被创建好之后,里面可能有多个执行流
与进程相比,线程在CPU执行时可能更加轻量化:pcb上下文肯定要切换,但是线程的地址空间、页表不用换;CPU调度时,看到的是LWP,也就是轻量级进程Light Weight Process
1.1 线程的优点
创建 一个新线程的代价要比创建一个新进程小得多- 与进程之间的切换相比,线程之间的
切换 需要OS的工作量更少 - 线程占用的
资源 比进程少得多
1.2 线程能够看到进程的所有资源,因为所有PCB都共享地址空间
- 好处:线程间通信成本低
- 坏处:存在大量的临界资源,势必需要使用各种互斥和同步机制保证临界资源的安全
1.3 线程异常
线程是进程的一个执行分支,当发生野指针/除0等异常操作导致线程退出 的同时,也意味着进程触发了该错误 ,操作系统会给进程发送信号,终止进程。这体现了多线程下鲁棒性降低了。
1.4 线程的共享与独享
线程共享
- 文件描述符表
- 每种信号的处理方式
- 工作目录
- 用户id组id
独有:
- 上下文数据(寄存器):体现了多个线程是可以切换的
- 独立的栈结构:体现了线程是独立运行的,各自的上下文数据不会互相影响
2.Linux线程控制:pthread线程库
首先要强调一点:pthread库并不是系统库,而是Linux下为了模拟线程而采用的第三方库。本质是封装了对于轻量级进程的某些操作。
链接这些线程函数库时要使用编译器命令的“-lpthread”选项 接口: pthread_create() pthread_join() pthread_cancel() pthread_exit() pthread_self()
2.1线程的创建
功能: 创建一个新的线程 原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
4个参数
- thread:返回
线程ID ,这是进程地址空间的共享区的地址 - attr:设置线程的属性,attr为
NULL 表示使用默认属性 - start_routine:是个函数地址,线程启动后要执行的函数
- arg:传给线程启动函数的参数,如果需要传入多个参数,可以用
结构体 封装
返回值: 成功返回0;失败返回错误码
让我们来玩一玩线程的创建,这边我们在main函数,也就是主线程创建了新线程,又在新线程中创建了另外5个更新的线程,使用ps -aL 命令查看轻量级进程
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* routine(void* args) {
while(true) {
cout << "I am a thread" << endl;
sleep(1);
}
return nullptr;
}
void* ThreadRoute(void* args) {
pthread_t tids[5];
for(int i = 0; i < 5; i++) {
pthread_create(tids+i, nullptr, routine, nullptr);
}
for(int i = 0; i < 5; i++) {
pthread_join(tids[i], nullptr);
}
return nullptr;
}
int main()
{
pthread_t tid;
int thread_id = pthread_create(&tid, nullptr, ThreadRoute, (void*)"I am thread");
while(true) {
std::cout << "I am main process" << std::endl;
sleep(2);
}
return 0;
}
2.2 线程的终止
- 从自己的例程中return,线程退出
主线程 退出,进程退出
有三种方法终止某个线程而不终止进程:
- 从线程函数
return 。这种方法对主线程不适用,从main函数return相当于调用exit。 - 线程可以调用
pthread_ exit 终止自己。 - 一个线程可以调用
pthread_ cancel 终止同一进程中的另一个线程
线程一般终止之后,main thread等待,不等待会造成僵尸
为防内存泄漏,要保证主线程最后退出,让新线程正常结束
retral从pcb中提取退出码
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
using namespace std;
void* ThreadRoute(void* args) {
int a = 0;
int b = 10;
b = b/a;
return nullptr;
}
int main()
{
if(fork() == 0) {
pthread_t tid;
int thread_id = pthread_create(&tid, nullptr, ThreadRoute, (void*)"I am thread");
exit(11);
}
int status;
pid_t id = waitpid(-1, &status, 0);
cout << "exit code: " << ((status>>8)&0xff) << endl;
cout << "sig: " << ((status)&0x7f) << endl;
return 0;
}
我们精心设计了除0错误: 在进程exit code中,存有退出码+信号,而信号是针对进程的,线程崩溃进程随之崩溃
子进程创建的线程的除0错误导致OS给子进程发送信号,子进程的主线程崩溃,退出码收不到
关于pthread_cancel
- sleep新线程被创建了但是没有被调度你就cancel了,建议一定要让新线程被调度跑起来
- cancel具有一定的延时性,并不一定立即执行
- cancel建议不要再开头或结尾使用
2.3 线程等待
为什么要等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
功能:等待线程结束 原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
- thread:线程ID
- value_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参数。
我们让线程return一个new出来的5
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
using namespace std;
void* ThreadRoute(void* args) {
int* p = new int(5);
cout << "threadID: " << pthread_self() << endl;
sleep(2);
return (void*)p;
}
int main()
{
if(fork() == 0) {
pthread_t tid;
void* ret;
int thread_id = pthread_create(&tid, nullptr, ThreadRoute, (void*)"I am thread");
pthread_join(tid, &ret);
cout << "ret: " << *(int*)ret << endl;
delete (int*)ret;
exit(11);
}
int status;
pid_t id = waitpid(-1, &status, 0);
cout << "exit code: " << ((status>>8)&0xff) << endl;
cout << "sig: " << ((status)&0x7f) << endl;
return 0;
}
2.4 获取线程ID
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
pthread_t pthread_self(void);
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
using namespace std;
void* ThreadRoute(void* args) {
cout << "threadID: " << pthread_self() << endl;
}
int main()
{
if(fork() == 0) {
pthread_t tid;
int thread_id = pthread_create(&tid, nullptr, ThreadRoute, (void*)"I am thread");
exit(11);
}
int status;
pid_t id = waitpid(-1, &status, 0);
return 0;
}
2.5 线程分离
线程分离的本质是让主线程不join新线程,不关心返回值,从而让新线程退出的时候自动回收
如果一个线程被设置为分离状态,他就不该被join 如果你join,结果就是未定义 即便线程被设置为分离状态,但是如果该线程依旧出错崩溃,还是会影响主线程和其他正常线程 -> 所有线程在同一个地址空间中运行
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
线程分离后,主线程等待不到新线程:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* arg)
{
pthread_detach(pthread_self());
printf("%s\n", (char*)arg);
return NULL;
}
int main(void)
{
pthread_t tid;
if (pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0) {
printf("create thread error\n");
return 1;
}
int ret = 0;
sleep(1);
if (pthread_join(tid, NULL) == 0) {
printf("pthread wait success\n");
ret = 0;
}
else {
printf("pthread wait failed\n");
ret = 1;
}
return ret;
}
3.总结补充
为什么要有pthread原生线程库?
linux没有真正的线程,是用进程来模拟的
操作系统是不会直接提供类似的线程创建、退出、分离、等待相关的system call 接口,但是会提供创建轻量级进程的接口,但是用户需要有所谓的线程创建、退出、分离、等待相关的接口啊,所以为了更好的适配轻量级进程的接口,就模拟封装了一个用户层 原生线程库NPTL
可是进程是PCB去管理的,用户层先要以管理线程的办法来管理轻量级进程,就得知道,线程id,状态,优先级,其他属性,从而用来进行用户线程管理!
所以tcb不用内核维护,而在用户层维护
曾经的pthead_t 是用户层的概念,是pthread库中的地址
相当于警察派了几个线人去犯罪集团当卧底,警察不懂行话,但是线人懂,所以线人充当了和犯罪分子沟通的角色,而线人反手会把情报用人话反馈给警察
警察只需要来操纵、管理线人,就能得到情报
|