线程基本理解
线程是进程内部的一个执行分支,是进程的一部分,一个进程下可能有多个线程,当一个线程崩溃时,同进程下的其它线程都会崩溃。进程是资源分配的基本单位,如果把进程比作公司里的一个部门,那它就是最基础的部门,再往下就是员工,没有更小的部门。而线程就是这个部门里的员工,每一个员工都可以使用部门里的资源,也就是线程会共享进程的资源。
线程是调度的基本单位。我们写的所有程序最终都会转化为二进制指令,CPU调度本质是在一组又一组的指令中切换。从CPU的视角看来,线程的指令就是最基本的一组指令。CPU每次要切换到不同组的指令时,就是以线程为单位,每次切换会执行不同线程的指令。线程使用CPU的时间也是用时间片确定,如果一个进程下有多个线程,进程拥有的时间片会等分给每个的线程。
其次,线程是单独调度的 ,CPU(严格来说是CPU的一个核心)不会同时运行两个线程,所以,每个线程有自己独立的上下文数据(即CPU运行时寄存器中存放的数据,如下一条指令的地址、临时数据等)。最后,线程有自己独立的栈结构,这个后面会讲解。
线程的数量比进程多得多,同样需要被操作系统管理,管理方式仍是先描述再组织。管理线程的数据结构被称为TCB。不同操作系统的线程实现方式不一样,在Linux系统中,线程就是轻量级的进程,或者用轻量级进程模拟实现的,它们的数据结构都是task_struct。Linux操作系统将二者统一视为执行流。
明明可以使用不同的进程来完成不同的任务,为什么还要再分出个线程呢?这与二者的切换效率有关。线程切换与进程切换都需要通过操作系统内核进行完整的上下文保存与恢复,但进程切换由于需要更换地址空间,CPU中的页表缓冲 TLB (快表)会被全部刷新,这将导致CPU对内存的访问在一段时间内相当的低效。此外还涉及到操作系统对PCB的切换。
即使线程的切换成本较低,也不应创建太多线程,尤其以大量计算数据为主的线程,频繁切换会影响计算效率。其次,线程缺乏访问控制,在一个多线程程序里,容易因为时间分配上的细微偏差或者共享了不该共享的资源而造成不良影响。
线程控制
Linux只提供了创建轻量级进程的系统调用,这是因为Linux系统不存在真正意义上的线程,只有轻量级进程。在Linux系统下,所谓的线程是用轻量级进程模拟的,需要pthread库来提供线程的接口,而pthread库底层封装的就是轻量级进程的相关系统调用。可以发现Linux的线程实现是在用户层实现的,被称为用户级线程,而pthread则被称为原生线程库。
pthread库中的接口调用成功后会返回0,但出错时不会像其它的C标准库函数一样,设置全局变量errno,而是将错误代码作为返回值返回。
phtread是第三方库,编译使用了该库的代码时要用-l选项指明。

创建线程 pthread_create
在Linux中,创建线程使用的是pthread_create,它不属于系统调用接口。

第一个参数thread是输出型参数,用于获取线程ID,attr用于设置线程属性,先设为nullptr即可,start_routine就是创建的线程要执行的函数,该函数的返回值和参数都是一个void*指针,arg则用于将参数传入start_routine指向的函数。
启动程序时,由操作系统创建的线程为主线程,由用户在程序中通过pthread_create接口创建的线程为新线程。新线程创建完成后,就会执行参数start_routine指向的函数,执行到return就是线程终止。主线程则在调用pthread_create接口处继续向下执行。
主线程中也可以调用新线程要执行的函数,这样就轻松重入了该函数。
cpp
#include<cstdio>
#include<unistd.h>
#include<pthread.h>
void* routine(void* vp){
char* temp=(char*)vp;
while(true){
printf(temp);
sleep(2);
}
return nullptr;
}
int main(){
pthread_t tid;
char fornew[]="我是新线程\n";
char formain[]= "我是主线程\n";
pthread_create(&tid,nullptr,routine,fornew);
routine(formain);
return 0;
}
等待线程 pthread_join
线程创建好后,新线程要被主线程等待,否则会出现类似僵尸进程的问题,造成内存泄漏。等待线程的接口是pthread_join

参数thread就是要被等待的线程的ID,retval是输出型参数,会传出该线程的返回值。
线程名设置与获取 pthread_setname_np、pthread_getname_np
给线程设置名称可以便于调试和系统监控,设置名称的接口是pthread_setname_np,参数thread是对应线程的ID,name是要设置的名称。

获取线程名则用的是pthread_getname_np,该接口会将线程名放在输出型参数name中,size用于指明name的大小。
获取线程ID pthread_self
使用pthread_self可以获取当前线程的ID

终止线程 pthread_exit
注意线程不能用exit()终止,它是用于终止进程的。终止线程要用pthread_exit接口,参数retval就是线程要返回的返回值。

取消线程 pthread_cancel
pthread_cancel接口用于取消线程,即让对应的线程终止,上面的pthread_exit是让调用该接口的线程终止。取消线程前要保证对应线程已经启动。线程被取消后,该线程的退出结果是PTREAD_CANCELED,即-1。

分离线程 pthread_detach
如果想让新线程结束之后自动回收资源,可以使用pthread_detach将新线程设置为分离状态,这样就可以让主线程分离新线程。被分离的线程依旧在进程的地址空间中,仍然可以访问进程的资源。

分离线程后就不能再用pthread_join接口等待该线程,否则会出错。
线程管理
pthread_self返回的"ID"是pthread库给每个线程定义的进程内唯一标识,是pthread库维护的,只能在进程内部使用。我们知道pthread库本质是用Linux提供的轻量级进程来模拟线程,在操作系统内部用于区分轻量级进程的标识是LWP(light weight process)。使用ps -aL命令可以查看当前的轻量级进程,其中LWP和PID相同的轻量级进程对应的就是主线程。

线程的概念是在pthread库中维护的,为了在库内部管理好各个线程,pthread库会以先描述再组织的形式管理线程,对应的数据结构是struct pthread。如下图所示,struct pthread、线程局部存储和线程栈共同组成线程控制块(TCB),其中struct pthread是核心管理结构,所以也可说struct pthread就是TCB。
知道这个结构后,我们就能轻松理解pthread库中的接口的底层逻辑,pthread_self返回的线程ID其实是TCB的起始虚拟地址。pthread_join接口就是用于释放这块空间的。而在struct pthread中,有一个成员joinable,当它的值为1时,线程退出后不会自动回收,为0则相反。pthread_detach接口内就是通过修改joinable来实现线程分离的。
由于动态库只会加载一个,而线程是在pthread库中维护,因而整个系统的所有线程都在库中。但每个进程只能拿到自己线程的控制块的虚拟地址。

线程局部存储(TLS)是让每一个线程都拥有全局变量、静态变量独立副本的机制。进程中的全局变量和静态变量所以线程都可以访问,但如果用__thread(两条杠)修饰,则新线程创建时会在TLS中存储该变量的副本,这样线程之间就不会共享该变量了。线程局部存储只能存储内置类型和部分指针。
cpp
__thread int count = 0;//全局变量
static __thread int size = 4;//静态变量
线程栈是线程自己独立的栈结构,用于存储线程的局部变量等数据,相当于线程自己的"栈区"。虽然Linux将线程(轻量级进程)和进程不加区分的用 task_struct 管理,但是对待其地址空间的栈还是有些区别的。 进程(主线程)的栈空间是可以动态增长的,而主线程中创建的新线程的线程栈的大小则是事先固定下来的。由于线程缺少访问控制,不同线程虽然有自己独立的栈,但只要能找到地址,任何线程都可以进行访问。
线程封装
下面我们将线程简单封装为一个类。封装线程时,线程要执行的函数routine不能是成员函数,否则会包含this指针,routine的参数只能是一个viod*指针。解决方法很简单,将routine设为static函数即可。但是缺少this指针无法访问成员变量,如果我们想要线程执行用户传入类中的函数,函数的函数指针肯定要保存在类内,类内成员必须通过this指针才能访问。为此,我们可以将在调用pthread_creat时,将this指针传入pthread_creat的参数arg,这样一来,在pthread_creat内部就会将this指针传入routine中,我们在routine中将其强制转换为类指针,然后访问里面的函数指针即可。
cpp
pthread_create(&_tid,nullptr,Routine,this);
下面是参考代码
Thread.hpp
cpp
#pragma once
#include <cstdio>
#include<iostream>
#include <unistd.h>
#include <pthread.h>
#include<functional>
#include<string>
namespace ThreadModule
{
enum WaitStatus{
Notenabled,
Runing,
Ended,//已完成或取消
Processed//已分离或回收
};
static int count = 0;//用于取线程名
class Thread
{
public:
Thread(void (*thefunc)(),bool joinable=false)
:_ws(Notenabled),
_func(thefunc),
_joinable(joinable)
{
count++;
_name="thread-";
_name+=std::to_string(count);
}
//创建并初始化线程
void Start(){
_ws=Runing;
//创建线程
pthread_create(&_tid,nullptr,Routine,this);
//设置线程名
pthread_setname_np(_tid,_name.c_str());
//是否分离线程
if(_joinable==true) Detach();
}
//取消线程,是否分离在初始化时已经处理,是否等待由用户自己选择调用Join,这里不用管
void Stop(){
pthread_cancel(_tid);
_ws=Ended;
}
//获取线程名
std::string GetName()
{
return _name;
}
//等待线程,若线程未运行结束则会阻塞等待
bool Join()
{
if(_joinable==true){
std::cout<<"线程已分离"<<std::endl;
return false;
}
else if(0==pthread_join(_tid,&_retval)){
_ws=Processed;
return true;
}
return false;
}
//分离线程
bool Detach(){
_joinable=true;
if(_ws==Notenabled) return true;//还没运行,上一行已经预设完,直接退
else if(_ws==Processed){
std::cout<<"线程已分离或回收"<<std::endl;
return false;
}
else if(pthread_detach(_tid)==0){
_ws=Processed;
return true;
}
else return false;
}
private:
//用于传入pthread_create函数,即线程要执行的函数
static void* Routine(void* th){
Thread* t=(Thread*)th;
t->_func();
t->_ws=Ended;//执行完用户传入的方法就算完成
return nullptr;
}
pthread_t _tid;
WaitStatus _ws;//线程状态
std::function<void()> _func;
bool _joinable;//预设是否分离线程
void* _retval;
std::string _name;
};
};
Main.cc
cpp
#include"Thread.hpp"
using namespace ThreadModule;
using namespace std;
void func(){
int n=3;
while(n--){
cout<<"正在工作中"<<endl;
sleep(1);
}
cout<<"已完成"<<endl;
}
int main(){
Thread th(func);
//th.Detach();
th.Start();
//th.Stop();
cout<<th.GetName()<<endl;
//while(true);//卡住主线程,防止主线程早退
th.Join();
return 0;
}