
一、线程基本概念1、认识线程提到线程那么我们不得不先回忆进程我们知道进程是在内存中执行的程序然后进程进程内核数据结构数据和代码。然后我们创建一个进程要给其创建对应的task_struct虚拟地址空间页表等。线程是在进程内部的执行分支。本质就是在进程的虚拟地址空间进行运行。线程属于操作系统调度的基本单位进程是承担分配系统资源的实体。下面我们直接学习在Linux中线程的原理和如何操作的。原理如下首先对于线程其是进程的一个执行分支但是其也是会有其标识符的但是呢其代码是复用的我们进程的那么会有一个task_struct这个叫做进程的TCB。然后其和进程的虚拟地址空间是共享的。进程内部会存在一个或者多个线程前面我们学习的进程其实就是只有一个执行流的进程也就是只有一个线程的进程。所以我们的进程的概念又要更新了进程多个线程内核数据结构代码和数据在我们CPU中需不需要将我们的进程和线程进行细分呢不需要线程是属于操作系统调度的基本单位在CPU角度来说只有线程没有进程只有执行流也就是线程然后线程在Linux中也称为轻量级进程。2、分页式存储管理我们的操作系统对于物理内存其读取都是按照4KB的单位进行读取的所以实际上对于物理内存的管理其是分为一个一个叶帧或者页框的。然后我们的虚拟地址空间和物理内存之间的映射就是按照4KB的单位进行映射的。那么我们的物理内存大小是固定的那么我们可以将物理内存进行划分那么就分为固定的分区就好比我们前面学习磁盘的时候进行分块的操作那么也可以通过下标的方式进行访问。就比如我们现在的物理内存是4GB那么我们按照个页框的大小为4KB那么就一共可以分为1024*10241048576个那么就可以按照数组下标的方式进行访问了。不过有个疑问就是我们的页表有个问题占用内存很大如果我们的进程使用了全部的内存也就是4GB那么我们的页表就要映射1048576个地址然后我们知道我们一个虚拟地址是32位的二进制数那么就需要4096个空间那么就是4MB的空间那么这个内存就太大了。实际上我们的虚拟地址也是分为三个部分的前十位其表示的是页目录中间10位表示的是页表那么页目录中其记录的是每一个页表然后呢我们的页表中其映射的是当前进程所使用到的物理内存页框。然后我们一个页表就是2^10个的映射关系然后我们的一个页目录可以映射2^10个页表那么就是1024*1024个物理内存的页框那么不就刚刚好是4GB的空间。这⾥的每⼀个表就是真正的⻚表所以⼀共有 1024 个⻚表。⼀个⻚表⾃⾝占⽤ 4KB 那么 1024 个⻚表⼀共就占⽤了 4MB 的物理内存空间和之前没差别啊 从总数上看是这样但是⼀个应⽤程序是不可能完全使⽤全部的4GB空间的也许只要⼏⼗个⻚表就 可以了。例如⼀个⽤⼾程序的代码段、数据段、栈段⼀共就需要 10 MB 的空间那么使⽤ 3 个 ⻚表就⾜够了。不过我们不需要考虑其是如何转换的这些工作是都由硬件来帮我们完成。3、线程的优点创建⼀个新线程的代价要⽐创建⼀个新进程⼩得多与进程之间的切换相⽐线程之间的切换需要操作系统做的⼯作要少很多 。最主要的区别是线程的切换虚拟内存空间依然是相同的但是进程切换是不同的。这两种上 下⽂切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能 损耗是将寄存器中的内容切换出。 ◦ 另外⼀个隐藏的损耗是上下⽂的切换会扰乱处理器的缓存机制。简单的说⼀旦去切换上下 ⽂处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚 拟内存空间的时候处理的⻚表缓冲 TLB 快表会被全部刷新这将导致内存的访问在⼀ 段时间内相当的低效。但是在线程的切换中不会出现这个问题当然还有硬件cache。线程占⽤的资源要⽐进程少 • 能充分利⽤多处理器的可并⾏数量在等待慢速I/O操作结束的同时程序可执⾏其他的计算任务计算密集型应⽤为了能在多处理器系统上运⾏将计算分解到多个线程中实现I/O密集型应⽤为了提⾼性能将I/O操作重叠。线程可以同时等待不同的I/O操作。4、线程的缺点性能损失 ◦ ⼀个很少被外部事件阻塞的计算密集型线程往往⽆法与其它线程共享同⼀个处理器。如果计 算密集型线程的数量⽐可⽤的处理器多那么可能会有较⼤的性能损失这⾥的性能损失指 的是增加了额外的同步和调度开销⽽可⽤的资源不变。健壮性降低 ◦ 编写多线程需要更全⾯更深⼊的考虑在⼀个多线程程序⾥因时间分配上的细微偏差或者 因共享了不该共享的变量⽽造成不良影响的可能性是很⼤的换句话说线程之间是缺乏保护 的。缺乏访问控制 ◦ 进程是访问控制的基本粒度在⼀个线程中调⽤某些OS函数会对整个进程造成影响。编程难度提⾼ ◦ 编写与调试⼀个多线程程序⽐单线程程序困难得多单个线程如果出现除零野指针问题导致线程崩溃进程也会随着崩溃线程是进程的执⾏分⽀线程出异常就类似进程出异常进⽽触发信号机制终⽌进程进程 终⽌该进程内的所有线程也就随即退出5、线程VS进程进程间是具有独立性的进程是线程的承载实体。线程间资源共享也就是共享进程间的资源线程间也有自己私有的资源线程ID一组寄存器存放其上下文数据栈空间等同⼀地址空间,因此 Text Segment 、 Data Segment 都是共享的,如果定义⼀个函数,在各线程中 都可以调⽤,如果定义⼀个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和 环境:⽂件描述符表每种信号的处理⽅式(SIG_IGN、SIG_DFL或者⾃定义的信号处理函数)当前⼯作⽬录用⼾id和组id其有如下四种关系二、Linux线程控制我们知道在Linux中其实是没有线程的而是轻量级进程在我们的C库函数中提供了一系列的函数使得我们用户层面上使用就和线程一样。下面我们通过线程的几个操作来学习这些库函数。1、线程创建与线程有关的函数构成了⼀个完整的系列绝⼤多数函数的名字都是以“pthread_”打头的要使⽤这些函数库要通过引⼊头文件pthread.h链接这些线程函数库时要使⽤编译器命令的“-lpthread”选项。因为这个是一个外部库有的编译器版本其不认识这个库所以我们为了系统可移植性在编译的时候都加上这个选项。然后我们创建一个线程需要使用到下面这个接口pthread_create其函数原型如下首先第一个参数其是返回我们的线程的ID的其实际上就是一个无符号长整型。第二个参数其是设置我们线程的属性的不过大部分情况下这个参数我们都不需要理直接设置为NULL即可。第三个参数就是我们要线程去执行的函数了然后我们还看到这个参数的类型是void类型的那么我们也可以传一个结构体。然后这个函数的返回值若线程创建成功那么其返回0如果创建失败那么其会返回错误码。这个函数对于错误检查也有点特点传统的⼀些函数是成功返回0失败返回-1并且对全局变量errno赋值以指⽰错误。pthreads函数出错时不会设置全局变量errno⽽⼤部分其他POSIX函数会这样做。⽽是将错 误代码通过返回值返回。pthreads同样也提供了线程内的errno变量以⽀持其它使⽤errno的代码。对于pthreads函数的 错误建议通过返回值业判定因为读取返回值要⽐读取线程内的errno变量的开销更⼩。下面我们通过代码来看其效果#include unistd.h #include stdlib.h #include stdio.h #include string.h #include pthread.h void *rout(void *arg) { int i; for (;;) { printf(Iam thread 1\n); sleep(1); } } int main() { pthread_t tid; int ret; if ((ret pthread_create(tid, NULL, rout, NULL)) ! 0) { fprintf(stderr, pthread_create : %s\n, strerror(ret)); exit(EXIT_FAILURE); } int i; for (;;) { printf(Iam main thread\n); sleep(1); } return 0; }我们上面的代码中就只创建了一个线程然后我们发现上面有两个PID是一样的。但是其对应的LWP不一样那么其就是两个线程了我们在命令行中输入ps -aL就可以查看到当前程序的线程了。不过我们也可以在程序中使用pthread_self(void)使用这个函数那么就可以获取到执行这部分代码的线程的ID了。其就是一个类似地址的东西。LWP是什么呢LWP得到的是真正的线程ID。之前使⽤pthread_self 得到的这个数实际上是⼀ 个地址在虚拟地址空间上的⼀个地址通过这个地址可以找到关于这个线程的基本信息包括线 程ID线程栈寄存器等属性。2、线程终止当我们不需要使用这个线程的时候那么我们有如下三种方式将其终止从线程函数return。这种⽅法对主线程不适⽤,从main函数return相当于调⽤exit。线程可以调⽤pthread_exit终⽌⾃⼰。⼀个线程可以调⽤pthread_cancel终⽌同⼀进程中的另⼀个线程。这个函数谁调用那么就终止谁。其没有返回值需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是⽤malloc分配的, 不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。pthread_cancel函数这个函数就是允许我们的线程终止掉其他的线程不过要注意的是我们的其他线程不能用来终止主线程。3、线程等待首先我们的新线程必须被我们的主线程等待若是我们的主线程不等待新线程回收那么可能会产生我们前面学习进程的时候类似僵尸进程的问题。然后和父子进程一样我们的父进程有时候也要知道我们子进程的任务执行的咋样所以需要将其退出信息进行获取所以我们的父进程需要回收子进程的退出信息那么就要等待子进程退出。那么我们线程也是如此我们创建多线程的目的是让其去完成任务任务是否完成完成的咋样我们有时候也需要关心。那么我们的主线程可以使用int pthread_join(pthread_t thread, void **value_ptr);使用这个函数那么就可以等待我们新线程的退出信息了。首先第一个参数就是我们要等待的线程的tid然后第二个参数其是一个输出型参数那么我们看到其是一个二级指针说明我们要传入一个一级指针。那么这个指针变量就会存储到我们要等待的线程的提出信息。下面是代码演示void *rout(void *arg) { std::string namestatic_castconst char*(arg); while(true){ printf(我是新线程:%s,tid:0x%lx,pid:%d\n,name.c_str(),pthread_self(),getpid()); sleep(5); break; } return (void*)10; } int main() { pthread_t tid; pthread_create(tid,nullptr,rout,(void*)thread -l); sleep(5); void*retnullptr; int npthread_join(tid,ret); if(n0) { std::coutnew thread joid success:(long long)retstd::endl; } return 0; }如上我们在新线程完成任务要退出的时候我们返回一个10那么我们的主线程在等待的时候那么理论上获取到的退出信息就是10。运行结果如下如果说我们的新线程早已退出然后我们主线程的代码还没执行到等待那部分那么我们是否还可以获取到新线程的退出信息呢实际上是可以获取到的我们的线程退出只是其内核资源被释放了但是我们的进程还在那么其退出信息是会保存在库中的那么我们的主线程去等待获取的时候也是可以获取到的。我们上面演示的是线程都是在没有出现异常的情况下的那么若是我们的线程出现异常那么又该如何呢前面我们提到了如果我们一个线程出现异常那么就会导致我们的进程直接被退出那么这个就不是我们线程要考虑的问题了而是进程的父进程来进行回收的了。调⽤该函数的线程将挂起等待,直到id为thread的线程终⽌。thread线程以不同的⽅法终⽌,通过 pthread_join得到的终⽌状态是不同的总结如下: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参数。4、分离线程我们知道当我们的主线程去等待新线程退出的时候那么我们的主线程就会阻塞等待那么对于一些我们并不关心其退出信息的新线程那么就会很影响效率了所以对于这种新线程那么我们可以将其进行分离操作不过要注意的是这个分离操作并不将其和我们的进程完成分离关系只是说我们不在关心其状态罢了。其还是和其他线程一样共享我们进程的虚拟空间代码资源等。就是告诉我们的操作系统该线程退出的时候自动将其释放。int pthread_detach(pthread_t thread);然后对于一个线程进行分离操作可以是这个线程自己进行的也可以是其他线程进行的。如果是自己要进行那么我们可以使用pthread_self传参。然后要注意的是我们的分离和join是冲突的对于一个线程我们不能又分离又join而是二者选其一。代码示例如下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; }三、线程ID及进程地址空间布局通过上面的学习还是有下面几个疑问pthread_t tid是啥线程退出退出信息void**获取到退出其在那里LWP、栈结构、tid在那边1、线程ID我们前面提到在我们的Linux下其实是不存在线程这个东西的而是称为轻量级进程在C语言中提供了thread库使得我们在Linux下也可以实现线程的效果。首先我们可库文件其也是一个磁盘级文件那么我们要调用这个库就要先将其加载到内存中然后其会映射到我们要使用这个库的进程的虚拟地址空间的。前面我们查看到的线程id我们发现其是一个和虚拟空间地址很像的无符号长整型实际上上面我们查看到的线程id就是一个地址。还有就是一个进程其未来会是有多个线程的那么我们也要将其进行管理。实际上这些管理信息是在我们的库中的在thread库中其含有我们对于线程管理的信息如上实际上我们线程的id的地址就是指向这一个一个管理信息的就是struct pthread结构体。这个库是会映射到我们进程虚拟地址空间的mmap区域的当我们创建一个线程的时候那么就会为这个线程创建一个线程的结构体那么我们在创建线程得到的tid其实就是指向这个结构体的起始地址的。然后在这个结构体中也包含了线程的退出信息线程退出的时候内核层面上只会释放进程空间里对应的轻量级进程的资源然后会把线程退出信息写回到库中我们的进程还在那么我们的线程库就还在那么其他的线程就可以通过调用join来获取到这个线程的退出信息。对应线程是否被分离其结构体中会存在一个指针当期指向自己的id的时候那么其就是被分离的状态。2、线程局部存储前面我们提到线程中大部分资源都是共享的不过有的变量其是只允许线程自己使用的每个线程都有一个自己独立的副本互不干扰。这就是线程的局部存储(TLS)。就是全局变量是线程之间共享的然后线程的局部变量是每个线程私有的资源当一个线程修改自己的局部变量是不会影响其他线程的。不会出现同步问题。当线程退出的时候那么对应的局部存储也会被释放。在多线程中我们使用_thread关键字修饰一个变量那么其就会变成一个局部存储变量。四、线程封装下面我们可以尝试自己进行线程的封装我们先创建两个文件一个是我们对于线程的封装的文件然后再创建一个Main文件对我们封装的线程进行测试首先我们创建一个线程类我们再将其封装在一个命名空间中然后我们的线程需要有一个tid然后还要一个名字然后还有其状态还有一个是否被分离的标识位。然后状态我们使用一个枚举类型来表示。然后我们还可以将线程的退出信息也设置一个基本结构如上所示。下面我们完成线程创建的模块线程创建我们还是调用pthread_create函数来实现。不过要注意的是这个函数我们还需要一个任务所以我们再弄一个函数即可但是当我们在线程类中创建一个任务其提醒我们的参数不匹配我们的线程函数要求的参数是一个void*类型但是我们的任务函数也是如此呀那么为啥会提醒参数不匹配呢这是因为在类中的方法会默认有一个参数就是this指针所以其实际上是有两个参数的那么如何解决呢我们可以将这个任务放置在类外进行定义那么还有一个方式就是将这个方法使用static进行修饰static 静态方法不属于对象属于类调用静态方法不需要创建实例自然没有对象地址可以传给方法因此不存在 this 指针但是这样又会有一个问题就是当我们想在这个方法中获取到我们线程的信息时我们又没有this指针那么又无法访问类成员变量了。那么我们可以将创建线程的第四个参数传入this指针。那么我们的任务函数中就可以获取到this指针了。我们再使用static_castThread*将这个参数转换为我们的线程类类型。那么线程创建的代码如下然后我们再封装线程等待下面是线程回收部分如上就是对于线程进行一个简单的封装。