【Linux系统】入门线程:线程介绍与线程控制

张开发
2026/5/22 19:43:43 15 分钟阅读
【Linux系统】入门线程:线程介绍与线程控制
文章目录一、线程是什么1. 简介2. 特点3. 线程与进程对比4. 可重入函数二、线程控制1. 创建线程2. 线程终止3. 线程等待4. 线程分离5. 线程id与线程地址空间的理解6. 简单的封装线程控制一、线程是什么1. 简介在一个程序里的一个执行流叫做线程更准确的定义是“一个进程内部的控制序列”。在CPU的视角内没有进程只有执行流线程之前我们学习的进程本质只有一个线程即只有一个执行流。实际上一个进程可以有多个线程至少有一个。进程是资源分配的基本单位而线程是操作系统调度和执行的基本单位进程 多个线程虚拟地址空间页表代码和数据既然一个进程可以有多个线程那么势必要对线程进程描述组织所以一定有相关的数据结构Windows仿照进程控制块PCB单独设计了一种线程控制块TCB而Linux中线程复用了进程的task_strcut数据结构进行描述所以严谨来说Windows才存在真正的线程Linux中不存在真正意义上的线程应该称之为“轻量级进程”Linux提供了轻量级进程相关的系统调用可是我们用户只想用线程怎么办于是有了pthread库——用户级线程库为我们提供管理线程的接口和参数向下调用Linux的轻量级进程系统调用。pthread是一个第三方库早期C库没有包含他编译时需要我们显示链接-lpthread现在新版本的C库可能已经包括他了。为了方便叙述本文中暂且认为Linux轻量级进程线程。2. 特点首先要知道的是一个进程的所有线程共享一份地址空间与页表也就是共享进程资源由于这一点创建新线程时不需要拷贝地址空间和页表创建新进程。所以创建一个新线程的代价比创建一个新进程小的多。同时因为进程切换需要丢弃cache线程切换不需要切换cache。与进程切换相比线程的切换需要操作系统做的工作少很多。在计算密集型应用中为了能在多核多CPU机器上效率更高可以将计算分解到多个线程中实现。线程也有缺点最主要的是缺乏保护因在时间分配上的细微差别或者资源使用冲突的可能性是很大的。这就需要线程互斥和线程同步操作了。当然要记得所有线程都是属于同一个进程的。单独某个线程出现异常崩溃或发送信号等情况影响的是整个进程所有线程进程挂掉了所有线程也就都挂了。3. 线程与进程对比进程具有独立性大部分资源是独占的。一个进程的所有线程共享虚拟地址空间也就共享大部分进程资源。因为是同一个地址空间所以代码段和全局数据段都是共享的。一个全局变量、全局函数各进程都能访问。除此之外各线程还共享文件描述符表信号动作表当前工作目录进程pid和用户id等等但是也有一些资源是每个线程单独拥有的线程tid线程自己的上下文数据函数栈帧线程局部存储后面讲errno信号屏蔽字调度优先级等等4. 可重入函数可重入函数是指可以被多个任务或执行流如中断处理、多线程同时安全调用的函数。其核心在于当函数正在执行时另一个执行流如中断或另一线程同时再次进入该函数不会产生数据错乱或逻辑错误。这意味着可重入函数不能调用全局资源如不能调用malloc或free不能访问全局或静态变量不能调用标准IO函数这种函数就是不可重入函数。反之如果一个函数只访问自己的局部变量或参数则称之为可重入函数。在后续讲解线程互斥、线程同步时的线程安全问题函数是否可重入是一个重要的问题。二、线程控制与线程有关的函数基本都属于pthread库使用这些函数需要包含头文件pthread.hgcc编译时需要使用选项-lpthread以下所有函数返回值为int类型都表示函数调用成功返回0失败返回错误码。命令ps -aL可以查到当前所有的轻量级进程。1. 创建线程参数thread输出型参数记录创建的新线程id。attr用来设置线程属性我们用户不必关心传递NULL即可。start_routine是一个参数为void*返回值为void*的函数是创建的新线程会去执行的函数。arg传递给start_routine的参数。2. 线程终止如果要只终止一个线程而不终止整个进程有三种方法线程函数内return线程调用pthread_exit终止自己这个函数的参数相当于线程的返回值线程调用pthread_cancel可以终止同一进程中的任意一个进程不建议使用需要注意的是不论return还是pthread_exit返回的指针指向的内存必须是全局的或是手动分配的。不能在线程函数的栈上分配否则线程函数退出后这块内存会被自动释放了。3. 线程等待进程需要被等待是因为他需要被父进程回收防止内存泄露获得退出信息否则会导致子进程的僵尸问题。线程等待的道理也是类似的主线程需要回收子线程的空间也可能需要获得新线程的执行结果。除此之外一个子线程的异常崩溃会杀死整个进程而子进程的崩溃父进程可以捕获并继续运行。等待线程的函数参数thread等待的线程idretval一个二级指针它指向的指针会指向线程的返回值。如果不关心线程的终止信息可以设为NULL如果thread线程是被别的线程用pthread_cancel异常终止的则retval指向的内容是常数PTHREAD_CANCELED即-1。4. 线程分离默认情况下新创建的线程是joinable的这代表线程退出后必须对其进行线程等待操作否则无法释放资源。如果不关心线程的退出情况不需要等待线程我们可以将线程设置为“分离状态”让线程退出时系统自动回收释放线但是一旦子线程被设置为分离状态主线程就不能提前退出。pthread_detach函数用于分离一个线程joinable和分离是冲突的一个线程不能既是joinable又是分离的。5. 线程id与线程地址空间的理解pthread库会给每个线程分配一个线程id这个id是pthread库给每个线程定义的进程内唯一标识此id的作用域是进程级而非内核级Linux系统中pthread_t 类型的线程id本质是一个地址是一个进程地址空间上的地址如图这是线程在进程地址空间的分布情况pthread动态库链接到内存共享区内部会给每个线程开辟一部分区域存放每个线程自己的数据。线程id就是每个线程自己的内存区域的起始一个字节地址函数pthread_self能返回当前线程的id如果一个全局变量用__thread修饰了则各个线程会各自开辟一份空间各自有一份·。互不干扰。这种就称之为线程局部存储要注意的是只能用来局部存储内置类型数据。除此之外我们还可以在系统层面给线程自己设定名字名字不能过长否则会设置失败综合演示#includeiostream#includepthread.h#includestring#includeunistd.h// 局部存储, 每个线程自己有独立的a变量__threadinta10;// 正常的全局变量所有线程都能看到并共享intb1;void*routine(void*args){pthread_t*idstatic_castpthread_t*(args);std::string namethreadstd::to_string(b);pthread_setname_np(*id,name.c_str());while(1){std::cout新线程id: *idstd::endl;sleep(1);}returnnullptr;}intmain(){pthread_t tid1,tid2;// 创建两个新线程新线程调用routine函数pthread_create(tid1,nullptr,routine,(void*)tid1);pthread_create(tid2,nullptr,routine,(void*)tid2);pthread_detach(tid2);// 分离tid2,系统会自动回收pthread_join(tid1,nullptr);// 等待回收线程tid1return0;}打印线程信息中的LWP可以认为是真正的“轻量级进程id”之前所说的pthread_t 类型的线程id本质就是虚拟空间中的一个地址。有一个线程的LWP和进程pid相同这个线程就是主线程。主线程的栈就在整个内存空间的栈上其他子线程的栈在共享区。6. 简单的封装线程控制// Thread.hpp#ifndef_THREAD_#define_THREAD_#includeiostream#includepthread.h#includeunistd.hstaticintgnumber1;// 想要让线程执行的任务类型但是线程函数必须是void*(*)(void*)类型// 所以要在线程函数内再封装真正想要让线程完成的任务usingcallback_tvoid(*)();classThread{private:// 类内普通成员函数会隐藏this参数因此必须加static防止隐式传thisstaticvoid*Thread_Routine(void*args){Thread*selfstatic_castThread*(args);pthread_setname_np(self-_tid,self-_name.c_str());self-_task();// 执行任务returnnullptr;}public:Thread(callback_t task):_task(task),_tid(-1),_joinable(true),_result(nullptr){_nameNewThread-std::to_string(gnumber);}voidStart(){// 我们想要在Thread_Rontine函数类调用_task完成任务可以把this自己传过去pthread_create(_tid,nullptr,Thread_Routine,this);}voidJoin(){if(_joinable){pthread_join(_tid,_result);std::cout线程已回收std::endl;}else{std::cerr线程不可被等待std::endl;}}voidDetach(){if(_joinable){pthread_detach(_tid);_joinablefalse;}else{std::cerr线程已被分离std::endl;}}~Thread(){}private:std::string _name;pthread_t _tid;callback_t _task;void*_result;bool_joinable;};#endif#includeThread.hppvoidtask1(){while(1){std::cout这是task1std::endl;sleep(1);}}voidtask2(){while(1){std::cout这是task2std::endl;sleep(1);}}intmain(){Threadth1(task1);Threadth2(task2);th1.Start();th2.Start();th1.Join();th2.Join();return0;}本篇完感谢阅读。

更多文章