文章目录
- 前言
- 1.初始Liunx下线程
- 2.关于虚拟地址的补充知识
- 3.线程的相关特点
- 1.线程的优点
- 2.线程的缺点
- 3.线程异常
- 4.线程和进程的比较
- 4.线程相关操作接口
- 线程控制相关接口
- 5.关于线程id的理解
前言
本文主要是对Liunx之下线程的前置知识铺垫,同时也是对之前进程的相关知识的补充。
1.初始Liunx下线程
在一些教材中线程被定义为是一个执行分支,执行粒度比进程更细,调度成本更低。内核观点认为:进程是分担系统资源的基本实体,线程是cpu调度的基本单位。这些是操作系统设计哲学,为啥要细分出线程呢?其实这是为了减少一些不必要的因为进程切换造成的系统消耗,我们学过计算机都是知道程序局部性原理,cpu当前的访问的代码和当前访问的代码的周边代码后面会有较大的概率被重新访问到,因此内存中有个叫做cache缓存的硬件。当CPU频繁的切换的进程,cache中之前进程的相关数据也被清理需要重新加载当前进程相关数据。这就大大较低了cpu执行效率,同时切换进程时,cpu中相关寄存器也需要进行切换指向新进程的相关代码数据和进程pcb,这也是一种消耗。
由此可以被划分为粒度更细的线程,从而提高整机执行效率。线程同样需要被操作系统所管理起来,因此也需要对应的数据结构将其描述组织起来,不同的操作系统有不同的设计方案,在Liunx中线程是复用了进程pcb的结构和进程调度算法,这样无疑会让线程的结构相对来说更加简单,便于维护,同样执行效率也会更高。Windows中线程控制块Tcb是重新设计了一套数据结构和管理方案,因此相对来说Windows中的线程更复杂,执行效率也会更低一点。
Linux中线程被称为轻量级进程。
代码示例
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<pthread.h>
void *thread1_run(void *args)
{
while(1)
{
printf("我是线程1,我正在运行\n");
sleep(1);
}
}
void *thread2_run(void *args)
{
while(1)
{
printf("我是线程2,我正在运行\n");
sleep(1);
}
}
int main()
{
pthread_t t1,t2;
pthread_create(&t1,NULL,thread1_run,NULL);
pthread_create(&t2,NULL,thread2_run,NULL);
while(1)
{
printf("我是主线程,我正在运行\n");
sleep(1);
}
}
当我们将上述代码运行起来,发现确实是3个执行流,执行了3个while循环。我们可以看到这3个线程的PID是一样的,也就是这3个执行流是同属于一个进程的。Liunx中线程只有主次之分,主线程的LWP和PID是一样的,这个LWP就是线程的tid,当程序就没有其他执行流的时候也就只有一个进程(轻量级进程),同样的也就是一个所谓的线程。这就和之前的进程相关的知识并不冲突。
Liunx中的线程和进程应该如图的样式,Linux中进程应该是众多执行流和系统为其分配的资源的合集。
2.关于虚拟地址的补充知识
之前,在介绍磁盘文件相关内容的时候提到过,内存与外设进行IO交换的时候都是以4KB大小的数据块为单位的,这样可以提搞整机执行效率。当往内存中加载的数据不足4kb时我们根据程序局部性原理将其周边的数据也加载到内存中,从而提高执行效率。进程相关代码数据是通过页表和物理内存建立联系的,其实页表可以细分为页目录和页表项。以32位机器为例,前10位表示页目录,中间10为表示页表项,后面12用来找到对应的数据块。
页表的寻址方式就是通过页目录先定位到页表项,这样就相当于找到了要访问的数据块起始地址,后面12位就是数据块内的偏移的地址了,通过这种方式可以准确找到任何一个数据块的块内地址。
这种寻址方式就是基地址加偏移量,这样设计页表可以大大节省空间,不用担心因为光是页表就会造成地址不够的情况。
3.线程的相关特点
1.线程的优点
1.创建一个新线程的代价要比创建一个新进程小得多.
2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多,线程占用的资源要比进程少很多,能充分利用多处理器的可并行数量.
2.线程的缺点
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
3.线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
4.线程和进程的比较
线程是共用进程地址空间,多线程中很多资源是共享的。但是有一部资源的线程独有的:线程ID,
一组寄存器(上下文数据),栈
等等。进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:文件描述符表,每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数),当前工作目录,用户id和组id.
4.线程相关操作接口
Liunx下,没有真正意义上的线程,而是用进程模拟线程(LWP)。因此Liunx不会直接提供创建线程的系统调用,它最多给我们提供创建轻量级进程的接口。因此Linux中有专门的线程库将相关接口进行封装,给用户提供进行线程控制的接口。我们在使用线程库的时候必须加上-lpthread选项。
线程控制相关接口
线程创建接口:pthread_create
第一个参数线程id ,第二个参数是设置线程相关属性的,使用默认属性可设置为nullptr,第三个参数函数地址,表示线程启动后要执行的函数,第四个参数是作为传递给线程执行函数的参数。线程创建成功返回0,失败返回-1。
之前学习进程的时候,我们知道进程是需要等待的,以便于知道进程执行情况。如果没有进程等待就可以会出现僵尸进程,同样的线程一般来说需要等待的,以便于让我们知道这个线程的执行结果。
pthread_join函数线程等待
第一个参数是线程id,第二个参数是输出型参数,用来获得线程退出情况。
线程退出有以下几种方式,调用exit函数退出进程,线程自然也就退出了。在线程执行函数中直接return也可以退出。另外还有两个函数也可以退出线程。
pthread_exit函数用于直接退线程,参数也是输出型参数,相当于return值。
pthread_cancel函数取消一个正在运行的线程,这个时候获得的线程退出码是为2的。
pthread_self函数是一个无参函数,是用来获得线程id的。哪个线程调用它,就获得哪个线程的id。
代码示例
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<pthread.h>
#include<iostream>
#include<string>
using namespace std;
#define NUM 10
class ThreaData
{
public:
ThreaData(const string&name,int id)
:_name(name),_id(id){}
~ThreaData(){}
public:
string _name;
int _id;
};
void *thread_run(void *args)
{
ThreaData*td=static_cast<ThreaData*>(args);
while(1)
{
cout<<"thread is runnig name"<<td->_name<<"id :"<<td->_id<<endl;
sleep(4);
break;
}
delete td;
pthread_exit((void*)2);
}
int main()
{
pthread_t tids[NUM];
for(int i=0;i<NUM;i++)
{
char tname[64];
snprintf(tname,64,"thread-%d",i+1);
ThreaData* td=new ThreaData(tname,i+1);
pthread_create(tids+i,nullptr,thread_run,td);
sleep(1);
}
void* ret=nullptr;
for(int i=0;i<NUM;i++)
{
int n=pthread_join(tids[i],&ret);
if(n!=0)
{
cout<<"thred quit: "<<(int64_t)ret<<endl;
}
}
cout<<"thread quit!"<<endl;
return 0;
}
上述代码已经调用了相关接口,我们可以让线程执行流去做对应的任务,我们也可获取的线程相关的执行结果。
线程分离
我们主线程在等待其他线程的时候就处于阻塞状态的,如果我们不关心线程的执行结果,不想让主线程被阻塞,我们可以将线程进行分离,由系统回收线程相关资源。pthread_detach就是线程分离函数,
注意当一个线程被分离以后就不能在join了。
5.关于线程id的理解
我们创建线程是通过线程库来创建的。线程库的动态库,需要加载的内存中。线程库中做好了对线程描述组织工作,线程TCP控制块已经被库管理好了。线程PID实际上就是线程库中对应的线程结构的起始地址。这样在上层用户直接使用提供好的相关接口操作线程即可。
我们看到的LWP是给用户看的id,看起来比较短。但是这个pthread_t id是库中线程控制块的起始地址因此看起来比较长。
所以线程都有自己的独立栈,主线程是使用的系统栈,新线程是使用库中的栈。