目录
线程概念
线程理解
地址空间(页表,内存,虚拟地址)
线程的控制
铺垫
线程创建
编辑
线程等待
线程异常
线程终止
代码
线程优点
线程缺点
线程特点
线程概念
线程是进程内部的一个执行分支,线程是CPU调度的基本单位~
以前我们学过:加载到内存的程序称为进程,并且还修正了进程的概念——内核数据结构+进程代码与数据~
在没有学习线程前我们的正文代码执行都是串行调用的,如果能做到并行调用会提高效率,而这就是我们学习线程的原因~
线程理解
通过上图我们是可以了解到创建一个进程的成本是很高的~
那假设我们把正文代码分为3份,创建进程(执行流)时不去创建页表,地址空间,让新出现的两个PCB共同指向第一个进程的地址空间,三个进程各自执行正文代码的各部分区域,达到并行的目的~
地址空间与地址空间上的虚拟地址,本质上也是一种资源~
如果我们想要设计线程,那就必须要让OS进行管理——先描述,再组织~
可是线程和进程有太多类似的地方了,创建的成本开销也大,所以在Linux下是没有真正的线程的,反而是用我们上面所想的那般以进程模拟线程~
这就是为什么CPU是以线程为调度单位的原因,以前我们都是觉得是以进程为调度单位的,那是因为里面只有一个执行流,而现在学习线程后就可以理解多个执行流被cpu所调度。
所以我们重新认识一下什么是进程(内核角度)——承担分配系统资源的基本实体~
我们也不用去刻意区分被调度的task_struct到底是线程还是进程,统一把其当作执行流~也叫做轻量级进程~
地址空间(页表,内存,虚拟地址)
我们先提出一个问题:OS要不要管理内存呢?——要的~
如上图所示OS把内存以4KB的数据块进行划分,我们的程序文件恰好也是以4KB的数据块划分的,这样内存与程序能达到以数据库相互对应的目的~而我们把这些4KB大小的数据块由称为页框/页帧~
我们把页框进行描述,里面存放各自属性的数据。假设内存大小为4GB,那么就可以划分1048576个4KB大小的页框,然后以数组的形式组织起来~
因此可以得证一件事:OS进行内存管理的基本单位为4KB~
我们熟悉内存管理后再回来深度理解多个执行流是如何进行代码划分的~毕竟总不能我们臆想就划分成功的吧?
首先页表肯定不是如此简单地把物理地址与虚拟地址放在一起就说明映射的,地址空间有4G(2^32)而页表如果要映射地址空间中的每个虚拟地址,那么在一行的映射中是要包含物理地址,虚拟地址,权限等数据的,假设这些大小为10byte,那一个页表岂不是要2^32 *10这么大?这成本也是不合实际的~
所以真正的页表是下面这种~
我们把虚拟地址划分为3个区域,其中前10位表示页目录,一共可以存放1024个数据~
中间10位表示页表项,每个页表都可以存放1024个数据~
欧克,我们再来梳理一遍~前面10位用来寻找在页目录中对应的页表项(即在哪个页表),中间10位用来查找 在对应页表内的哪一个数据,里面存放着程序代码所属代码块的物理地址(页框的物理地址),最后12位是偏移量,通过里面存放的物理地址+该偏移量就能找到在对应数据块内部真正代码的位置(页内偏移)~ 然后在页表添上权限,与cpu一同判定为内核态还是用户态~
而这才是真正的页表映射~
给不同线程分配不同的区域,本质上就是让不同的线程各自看到全部页表的子集~
线程的控制
铺垫
Linux下只有轻量级进程而没有线程是为了减少开发成本,而为了给用户提供线程是引进了线程库供我们使用。
线程创建
test_thread:testThread.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm -f test_thread
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void *newthreadrun(void *args)
{
while (true)
{
std::cout << "I am new thread, pid: " << getpid() << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, newthreadrun, nullptr);
while (true)
{
std::cout << "I am main thread, pid: " << getpid() << std::endl;
sleep(1);
}
}
在同一程序下,进行了并行调用~
另外这里我们看到了一个新东西:LWP(轻量级进程),而操作系统在进行调度的时候是以LWP进行的,以前只有一个执行流时那么LWP==PID~
tid用于在库中维护,类似前面信号所学的mid,而LWP是用于内核中标识线程的,类似于key~
下面我们再来介绍一个函数:pthread_self(用来查看当前线程的tid)
//把地址转16进制
string ToHex(pthread_t tid)
{
char id [64];
snprintf(id,sizeof(id),"0x%lx",tid);
return id;
}
void *newthreadrun(void *args)
{
while (true)
{
cout << "I am new thread, pid: " << getpid() << " ToHex(tid):"<<ToHex(pthread_self())<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, newthreadrun, nullptr);
while (true)
{
cout << "I am main thread, pid: " << getpid() << "new thread tid: "<<ToHex(tid)<<"main thread tid"<<ToHex(pthread_self())<<endl;
sleep(1);
}
}
这里也验证了新线程确实是在主线程这里被创建的~
ps:关于新线程与主线程谁先运行这是不确定的,由调度器决定~
最后我们再来做个小demo:让主线程先退出,查看新线程情况~
//把地址转16进制
string ToHex(pthread_t tid)
{
char id [64];
snprintf(id,sizeof(id),"0x%lx",tid);
return id;
}
void *newthreadrun(void *args)
{
string threadname = (char*)args;
int cnt = 5;
while (cnt)
{
cout << threadname << " is running: " << cnt << ", pid: " << getpid() << " ToHex(tid):"<<ToHex(pthread_self())<<endl;
sleep(1);
cnt--;
}
return nullptr;
}
int main()
{
pthread_t tid;
//将字符串"thread-1"传递给形参args
pthread_create(&tid, nullptr, newthreadrun, (void*)"thread-1");
//主线程提前退出
cout<<"main thread quit"<<endl;
return 0;
}
由此验证一件事:主线程退出==进程退出==所有线程都要退出
线程等待
线程退出也是需要”wait“的,否则就会和进程那样发送内存泄漏的情况~
功能:等待线程结束原型int pthread_join ( pthread_t thread , void ** value_ptr );参数thread : 线程 IDvalue_ptr : 它指向一个指针,后者指向线程的返回值返回值:成功返回 0 ;失败返回错误码
1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
void *newthreadrun(void *args)
{
string threadname = (char*)args;
int cnt = 5;
while (cnt)
{
cout << threadname << " is running: " << cnt << ", pid: " << getpid() << " ToHex(tid):"<<ToHex(pthread_self())<<endl;
sleep(1);
cnt--;
}
return nullptr;
}
int main()
{
pthread_t tid;
//将字符串"thread-1"传递给形参args
pthread_create(&tid, nullptr, newthreadrun, (void*)"thread-1");
int n = pthread_join(tid,nullptr);
cout<<"main thread quit, n="<<n<<endl;
sleep(5);
return 0;
}
回收成功~
void *newthreadrun(void *args)
{
string threadname = (char*)args;
int cnt = 5;
while (cnt)
{
cout << threadname << " is running: " << cnt << ", pid: " << getpid() << " ToHex(tid):"<<ToHex(pthread_self())<<endl;
sleep(1);
cnt--;
}
return (void*)123;
}
int main()
{
pthread_t tid;
//将字符串"thread-1"传递给形参args
pthread_create(&tid, nullptr, newthreadrun, (void*)"thread-1");
//线程回收
void* ret = nullptr;
int n = pthread_join(tid,&ret);
cout<<"main thread quit, n="<<n<<" ret = "<<(long long)ret<<endl;
sleep(5);
return 0;
}
回收同时取得线程的返回值~
线程异常
线程退出有以下三种结果:
- 代码跑完,结果正确
- 代码跑完,结果错误
- 出现异常
而我们重点放在异常上面~
void *newthreadrun(void *args)
{
string threadname = (char*)args;
int cnt = 5;
while (cnt)
{
cout << threadname << " is running: " << cnt << ", pid: " << getpid() << " ToHex(tid):"<<ToHex(pthread_self())<<endl;
sleep(1);
cnt--;
//人为造异常
int*p = nullptr;
*p =1;
}
return (void*)123;
}
int main()
{
pthread_t tid;
//将字符串"thread-1"传递给形参args
pthread_create(&tid, nullptr, newthreadrun, (void*)"thread-1");
//测试异常
while(true)
{
sleep(1);
}
//线程回收
void* ret = nullptr;
int n = pthread_join(tid,&ret);
cout<<"main thread quit, n="<<n<<" ret = "<<(long long)ret<<endl;
sleep(5);
return 0;
}
我们发现当新线程出现异常后,连主线程都没了。
重点:在多线程中任意一个线程出现异常,都会导致整个进程退出!这也意味着线程的健壮性不是很好。并且我们还注意到因为线程异常我们连其返回值都接收不到了,因为异常导致所有线程都退出,是走不到回收函数的,整个进程都崩溃了,所以异常时往往不考虑回收了~
线程终止
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit函数
功能:线程终止原型void pthread_exit(void *value_ptr);参数value_ptr:value_ptr 不要指向一个局部变量。返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
void *newthreadrun(void *args)
{
string threadname = (char*)args;
int cnt = 5;
while (cnt)
{
cout << threadname << " is running: " << cnt << ", pid: " << getpid() << " ToHex(tid):"<<ToHex(pthread_self())<<endl;
sleep(1);
cnt--;
}
pthread_exit ((void*)123);
}
int main()
{
pthread_t tid;
//将字符串"thread-1"传递给形参args
pthread_create(&tid, nullptr, newthreadrun, (void*)"thread-1");
//线程回收
void* ret = nullptr;
int n = pthread_join(tid,&ret);
cout<<"main thread quit, n="<<n<<" ret = "<<(long long)ret<<endl;
sleep(5);
return 0;
}
功能:取消一个执行中的线程原型int pthread_cancel(pthread_t thread);参数thread: 线程 ID返回值:成功返回 0 ;失败返回错误码
int main()
{
pthread_t tid;
//将字符串"thread-1"传递给形参args
pthread_create(&tid, nullptr, newthreadrun, (void*)"thread-1");
//2s后把新线程终止
sleep(2);
pthread_cancel(tid);
//线程回收
void* ret = nullptr;
int n = pthread_join(tid,&ret);
cout<<"main thread quit, n="<<n<<" ret = "<<(long long)ret<<endl;
sleep(5);
return 0;
}
线程优点
最重要的是前2点:其中第一点不用我们多说,由于线程共享地址空间的缘故它们可以共享大部分资源~
再来谈谈第二点~
我们的认知是进程切换时需要在CPU中的寄存器重新记录数据~
后面有了cache后直接在这里取数据更便捷了,但归根到底进程切换还是得考虑cache的记录,因为每一次进程都是不一样的。不过线程就不需要顾虑太多了,它切换后cache概率上还能用~
线程缺点
线程特点
线程私有
- 线程的硬件上下文(CPU寄存器的值)
- 线程的独立栈结构
线程公有
- 代码和全局数据
- 进程文件描述符