文章目录
- 线程
- 线程原理
- 页表
- 线程VS进程
- 线程相关函数
- pthread_create函数
- pthread_self
- pthread_exit
- pthread_cancel
- pthread_join
- pthread_detach
- 线程ID
线程
什么是线程?为什么要有线程?
线程本质上就是轻量化的进程,一个进程就是一个执行流,在同一时间内只能去做一件事,而我们要是能把一个进程分为多个小的进程,那么就相当于把它分为多个执行流,这样在同一时间内我们就可以让一个进程去在同一时间内左不同的事情,而这个被分成的小进程就称为线程。
线程原理
我们要重谈一下进程地址空间,在之前我们通过fork函数来创建子进程,这个是不属于多线程的,因为子进程仍具有自己独立的虚拟地址空间,父子进程通过各自的页表映射到同一份物理内存,当一方发生数据改变的时候,就会进行写时拷贝,并更改页表的映射关系
当父子进程加载到内存里面时,因为他们之间数据很多是相同的,所以数据就会被加载两份,占据很大一部分空间,那么如何提高这个效率呢?不让相同的数据加载两份呢?
多线程,多线程相比于父子进程或者其他进程之间,是通过同一份虚拟地址空间来存储这个进程中所有线程的地址的。代码段,未初始化变量,已初始化变量等,在不同线程看来都是看到的同一份,所以只需要一份就可以了,对于线程之间不同的数据,操作系统会对他们进行划分,不同线程去拥有各自的虚拟内存,并让所有的线程通过同一份页表去映射到物理内存,这样就极大的提高了内存的利用率。
页表
为什么说通过多线程去做一件事,比多进程占用的内存更少呢?因为多线程是一个进程的细分,因此他只需要加载一个页表到内存,而多进程需要加载多个页表。我们一直提页表,那么页表的结构是怎么样的?虚拟地址是怎么转换成物理地址的?
我们以虚拟地址是32位为例,假设我们直接就是在一张表中一个虚拟地址对应一个物理地址,再加上权限等信息,那么一个条目大概就需要10字节,而虚拟内存有4GB,那么光页表需要的内存可能就需要40GB,这是不现实的
因此我们把页表划分成多级页表,一个虚拟地址是有32位的,我们将这32位划分成10+10+12位分别用来存放不同的信息
1-10位:用于找到该虚拟地址对应的二级页表
11-20位:用于找到该虚拟地址在二级页表中对应的页表项位置
21-32位用于存放该虚拟地址在物理内存中的偏移量
我们来对上面的映射过程进行总结:
一级页表中每个条目存放的是不同二级页表的起始地址,找到对应的二级页表后,在通过11-20位找到它在二级页表中对应的位置,二级页表中每个条目存放的是它在物理内存中对应的起始地址即找到对应的页框,而物理内存中每个页框大小为4KB,我们再通过21-32位,计算出虚拟地址在该页框中的偏移量。
每个页表对应的条目有2^10=1024个
页框大小为4KB,因此我们在通过21-32位计算出偏移量的时候2^12 = 4 * 2 ^10 = 4KB,因此不会超出该页框大小
线程VS进程
创建线程为什么要比进程更加轻量化?
因为创建线程只需要创建对应的PCB即可,虚拟内存和页表都与主线程共用一份即可。
为什么线程切换比进程切换效率更高?
如果进程切换就要重新加载数据(页表和虚拟内存等),且CPU中通过cache来存储缓存的数据,如果是进程间的切换就要重新缓存,并将数据由冷变热,而线程之间的切换,数据基本是不变的,因此不用再重新热数据
进程是资源分配的基本单位
线程是调度的基本单位
在Linux中线程和进程之间没有明确的划分
线程共享进程数据,但也拥有自己的一部分数据:
1.线程ID
2.一组寄存器(上下文数据)
3.栈
4errno
5.信号屏蔽字
6.调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
1.文件描述符表
2.每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
3.当前工作目录
4.用户id和组id
线程相关函数
pthread_create函数
int pthread_create(pthread_t* thread, const pthread_attr_t attr, void(start_rountinue)(void*), void *arg);
pthread_create函数用来创建线程
说明:线程被创建后将立即执行
参数:
thread:它的类型是pthread_t,底层是无符号长整型,这是一个输出型参数,用于得到该线程ID,我们后面在对线程的操作就是通过该ID来操作的,我们后面再细讲这个无符号长整型,先说明它是一个指向共享区的一块线程起始地址
attr:用于设置线程的属性,我们一般设为NULL,表示使用默认属性
start_rountine:这是一个函数指针,指向一个参数为void*,返回值为void的函数
arg:它的类型是void的,该变量用做start_rountine的参数
返回值:
创建成功返回0,失败返回错误码,注意这里不是返回-1,并设置错误码errno
为什么要这么做?
错误检查:
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小
创建线程等函数都是被包含在动态库pthread.h中的,因此在对该程序进行链接时,要加上-lpthread选项,去指明要链接的库
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;void* routine(void* arg)
{cout << "I am a thread" << endl;sleep(5);
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine, nullptr);sleep(5);return 0;
}
while :; do ps -eLf | head -1 && ps -eLf | grep mytest | grep -v grep; sleep 1;done
我们通过打印发现,同一个PID,对应有两个不同的轻量级进程,说明我们创建线程成功了,LWP和PID相同的被称为主线程,LWP:light weight process,轻量级进程
pthread_self
获取线程ID,我们可以通过创建线程的第一个参数来获取,也可以通过pthread_self函数来获取。
参数为void
返回值:该函数总是成功,返回的是当前线程的ID
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>using namespace std;void* routine(void* arg)
{cout << "other thread" << pthread_self() << endl;//通过函数获取该线程tid
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine, nullptr);cout << "other thread ,tid:" << tid << endl;//通过参数获取刚创建的线程tidcout << "main thread ,tid:" << pthread_self() << endl;//通过函数获取主线程tidsleep(1);return 0;
}
pthread_exit
线程退出函数,用于退出当前线程
参数,是一个输出型参数
返回值为空。
pthread_cancel
用于退出某一个线程
参数pthread_cancel:要去退出线程的线程ID(tid)
返回值:成功返回0,失败返回一个不为0的错误码
pthread_join
线程等待函数,用于阻塞式等待回收线程
参数:
thread:类型是pthread_t,用于指明要去等待线程的ID,也就是tid
retval:是一个void**类型的二级指针,最终指向的是线程的返回值,如果不关心设为nullptr即可
返回值:成功返回0,失败将返回错误码
对于不同情况的退出,参数retval的返回值也不同:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>using namespace std;void* routine(void* arg)
{cout << "other thread" << pthread_self() << endl;//通过函数获取该线程tid
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine, nullptr);cout << "other thread ,tid:" << tid << endl;//通过参数获取刚创建的线程tidcout << "main thread ,tid:" << pthread_self() << endl;//通过函数获取主线程tidpthread_join(tid, nullptr);return 0;
}
在不添加pthread_join(tid, nullptr);的情况下,默认是不等待线程退出的,线程都没来得及去执行routine函数
pthread_detach
分离线程,因为主线程要去回收其余线程,且是以阻塞的方式进行等待,这就会降低CPU的利用率,因此如果我们并不关系线程的返回值是什么,我们可以通过pthread_detch函数来分离线程,就是让线程自动的去释放它对应的资源,而不用再让主线程进行回收
参数:
thread:要去分离线程的ID
返回值:
成功返回0,失败返回对应的错误码
线程ID
LWP是底层线程对应的PCB结构体的唯一标识符
tid是上层每一个线程在共享区中的起始地址
LWP:什么是轻量级进程?就是线程。那么相对于我们之前学的进程ID和这个轻量级进程ID有什么关系呢?又和我们前面创建进程时的函数调用pthread_self()的返回值线程ID有什么区别呢?
每一个线程,它在内核中都对应有一个进程描述符,这个内核中的进程描述符就是LWP,我们在应用层又要求同一个进程中的线程pid的返回值相同,这又是怎么做到的呢?
Linux下的轻量级进程是一个PCB,每个轻量级进程都有一个自己的轻量级进程ID(PCB中的pid,也就是LWP),而同一个程序中的轻量级进程组成线程组,拥有一个共同的线程组ID
同时每一个线程又有一个tid,这个tid指向用户层的tcb,在加载到共享区的线程库中,每一个线程都有它对应的tcb结构
每个进程中都有一个LWP与线程组ID相等的线程,这个线程被称为主线程,因此当一个进程中只有一个线程时,通过LWP和进程pid来找到调度该线程是等价的的。
pthread_create是一个库函数,功能是在用户态创建一个用户线程,而这个线程的运行调度是基于一个轻量级进程实现的。
对于线程的创建,我们是通过原生线程库给我们提供的应用层接口来实现的,我们要先把原生线程库加载到共享内存当中,然后在这个共享内存中创建线程,而每一个线程都在共享内存中对应一个起始地址,这里的pthread_create的第一个参数就是这里的tid,也就是一个共享内存地址。
通过__thread设置线程局部存储,该变量属于每个线程的私有变量