【Linux】多线程入门详解
背景知识:
1.每次进程切换,都存在资源的保持和恢复动作,即上下文切换
2.进程的引入虽然可以解决多用户的问题,但是进程频繁切换的开销会严重影响系统性能
3.同一个进程内部有多个线程,这些线程共享的是同一个进程的所有资源
4.通过线程可以支持一份应用程序内部的并发,免去了进程频繁切换的开销
5.线程的切换是轻量级的,所以可以保证足够快
6.即使是单核计算机,也可以通过不停的在多个线程的指令间切换,从而造成多线程同时运行的效果
7.操作系统一般都有一些系统调用来让一个函数运行成为一个新的线程
8.对于多线程来说,由于同一个进程空间中存在多个栈,任何一个空白区域填满都会导致栈溢出
9.多线程与栈密切相关
一.线程创建与结束
相关函数:
1)int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void*(*start_routine)(void*),void *arg)
线程创建函数
参数1:*thread,需要创建的线程ID指针
参数2:*attr,用来设置线程属性
参数3:void*,线程运行函数的起始地址,页就是告诉线程你的线程运行函数是哪一个
参数4:*arg,线程运行函数的参数
函数的返回值int是函数是否运行成功的结果,当返回值为0表示运行成功,-1表示运行失败
当线程运行函数的参数不止一个时,需要将这些参数封装成一个结构体传进去
2)int pthread_join(pthread_t thread,void **retval)
调用线程等待thread线程运行结束,并且获得thread线程的返回值
参数1:thread,被等待线程的线程ID
参数2:用来存储thread线程的返回值
该函数一般是主线程调用,用来等待子线程运行完毕,函数的返回值int是函数是否运行成功的结果,当返回值为0表示运行成功,-1表示运行失败
3)void pthread_exit(void *retval)
结束当前线程,并返回一个返回值
参数1:*retval,线程结束的返回值
一般pthread_exit和pthread_join配套使用,获得子线程的返回值
样例程序:
#include <iostream> #include<pthread.h> using namespace std; void* say_hello(void* args) { cout<<"hello from thread"<<endl; pthread_exit((void*)666); } int main() { pthread_t tid; int iRet=pthread_create(&tid,NULL,say_hello,NULL); if(iRet) { cout<<"pthread_create error:iRet="<<iRet<<endl; return iRet; } void *retval; iRet=pthread_join(tid,&retval); if(iRet) { cout<<"pthread_join error:iRet="<<iRet<<endl; return iRet; } cout<<(long)retval<<endl; return 0; } /* hello from thread 666 */
先创建并运行一个子线程,在主线程中等待子线程运行结束,并且获取子线程的返回值然后输出
ps:调用pthread_join函数,获取线程的返回值!
二.向线程传递参数
在创建线程的时候可以向线程传递参数,pthread_create函数的第四个参数即为线程运行函数的参数,当要传递的参数有多个时,需要将这些参数封装起来然后传递
#include <iostream> #include<pthread.h> using namespace std; void* say_hello(void* args) { int x=*(int*)args; cout<<"hello from thread,x="<<x<<endl; pthread_exit((void*)666); } int main() { pthread_t tid; int para=123; int iRet=pthread_create(&tid,NULL,say_hello,¶); if(iRet) { cout<<"pthread_create error:iRet="<<iRet<<endl; return iRet; } void *retval; iRet=pthread_join(tid,&retval); if(iRet) { cout<<"pthread_join error:iRet="<<iRet<<endl; return iRet; } cout<<(long)retval<<endl; return 0; } /* hello from thread,x=123 666 */
三.获取线程的ID
1)调用pthread_self函数来获取当前运行线程的id,该函数的返回值是当前运行线程的id
2)在创建线程时直接获取创建的线程的id
四.线程的属性
typedef struct { int etachstate; //线程的分离状态 int schedpolicy; //线程调度策略 structsched_param schedparam; //线程的调度参数 int inheritsched; //线程的继承性 int scope; //线程的作用域 size_t guardsize; //线程栈末尾的警戒缓冲区大小 int stackaddr_set; //线程的栈设置 void* stackaddr; //线程栈的位置 size_t stacksize; //线程栈的大小 }pthread_attr_t;
1)线程的分离状态:线程的分离状态决定一个线程以什么样的方式的来终止自己
1.非分离状态:线程的默认属性是非分离状态,这种情况下,父线程等待子线程结束,只有当pthread_join函数返回时,子线程才算终止,才能释放自己占用的系统资源
2.分离状态:分离线程没有被其他线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源,可以根据自己的需要,选择适当的分离状态
3.怎么使得线程分离?
方法1:直接将线程设置为分离线程
pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)
第二个参数可选为:PTHREAD_CREATE_DETACHED(分离线程)和PTHREAD_CREATE_JOINABLE(非分离线程)
这里需要注意一点的是,如果设置一个线程为分离状态,而这个线程又运行得非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和资源交给其他线程使用,这样调用pthread_create就得到了错误的线程号,要避免这种情况可以采用一定的同步措施,比如在被创建的线程的运行函数中调用pthread_cond_timewait函数,使得线程睡眠几秒,留出足够的时间让pthread_create返回,设置一段等待时间,这是多线程编程中常见的方法,但是注意不要使用诸如wait的函数,他们是使得整个进程睡眠,并不能解决线程同步问题!
方法2:在需要分离的线程的运行函数中调用pthread_detached函数
int pthread_detach(pthread_t tid);若成功则返回0,若出错则为非零。
pthread_detach用于分离可结合线程tid。线程能够通过以pthread_self()为参数的pthread_detach调用来分离它们自己。
即:pthread_detach(pthread_self())
2)线程的栈地址
当进程栈地址空间不够时,,指定新建线程使用malloc分配的空间作为自己的栈空间,通过pthread_attr_setstackaddr和pthread_attr_getstackaddr两个函数分别设置和获取线程的栈地址,传给pthread_addr_setstackaddr函数的地址是缓冲区的低地址(不一定是栈的开始地址,栈可能从高地址往低地址增长)
3)线程的栈大小
1.当系统中有很多线程时,可能需要减小每个线程的栈空间默认大小,防止进程的地址空间不够用
2.当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增加线程默认栈的大小
3.函数pthread_attr_getstacksize和 pthread_attr_setstacksize提供设置。
4)线程的栈保护区大小
1.在线程栈的栈顶预留一段空间,防止栈溢出
2.当栈指针进入这段保护区时,系统会发出错误,通常是发送信号给线程
3.该属性的默认值是PAGESIZE大小,该属性被设置时,系统会自动将该属性大小补齐为页大小的整数倍
4.当栈改变地址属性时,栈保护区大小通常清零
5)线程的优先级
1.新线程不继承父进程的优先级,新线程的优先级默认为0
2.新线程使用SCHED_OTHER调度策略,即:线程一旦开始运行,直到被抢占或者直到线程阻塞或停止为止
6)线程的争用范围
1)PTHREAD_SCOPE_SYSTEM:此线程与系统中的所有线程进行竞争
2)PTHREAD_SCOPE_PROCESS:此线程与进程中的其他线程进行竞争
具有不停争用范围的线程可以在同一个线程甚至同一个进程中共存,进程范围只允许这种与同一进程中的其他线程争用资源,而系统范围则允许此类线程与系统中其他所有线程争用资源
7)线程的绑定状态
轻进程:可以理解为内核线程,位于用户层和系统层之间,系统对线程资源的分配,对线程的控制都是通过轻进程实现的,一个轻进程可以控制一个或多个线程
1.非绑定状态
默认情况下,启动多少个轻进程,哪些轻进程来控制哪些线程都是由系统来控制的,这种情况称之为非绑定
2.绑定状态
即某个线程绑定在一个轻进程之上,被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用,通过被绑定的轻进程的优先级和调度级可用使得绑定的线程满足诸如实时反应之类的要求
五.线程分离
1.将线程设置为结束状态分离后,线程的结束状态不能被进程中的其他线程得到,同时保存线程结束状态的存储区也将变得不能应用
样例1:
在一个线程被创建之前分离线程
#include <iostream> #include<pthread.h> #include<stdio.h> #include<stdlib.h> #include<string.h> using namespace std; void * thread_run(void * arg) { cout<<"the thread"<<endl; return NULL; } int main(void) { int flag; pthread_t tid; pthread_attr_t attr;//attrbute flag=pthread_attr_init(&attr);//thread init if(flag) { cout<<"can‘t init attr "<<strerror(flag)<<endl; return flag; } flag=pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//setting thread detachment status if(flag) { cout<<"can't set attr "<<strerror(flag)<<endl; return flag; } flag=pthread_create(&tid,&attr,thread_run,NULL); if(flag) { cout<<"can't create thread "<<strerror(flag)<<endl; return flag; } flag=pthread_join(tid,NULL); if(flag) { cout<<"can't join thread and thread has been detached"<<endl; return flag; } return 0; } //answer:can't join thread and thread has been detached
分析:由于子线程分离,因此得不到子线程的结束状态信息,pthread_join函数会出错,并且由于子线程分离,子线程运行函数中输出的字符对主线程而言也是不可见的,所以字符串”the thread“没有被打印出来!
样例二:分离一个已经创建的线程
#include <iostream> #include<pthread.h> #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> using namespace std; void * thread_run(void * arg) { cout<<"the son thread sleeping for 5 seconds"<<endl; sleep(5);//sleeping 5 seconds and wait fotr the main thread to set the thread to a separate state cout<<"the son thread done"<<endl; return NULL; } int main(void) { int flag; pthread_t tid; flag=pthread_create(&tid,NULL,thread_run,NULL); if(flag) { cout<<"can't create thread "<<strerror(flag)<<endl; return flag; } flag=pthread_detach(tid);//detach a thread if(flag) { cout<<"can't detach thread "<<strerror(flag); return flag; } flag=pthread_join(tid,NULL); if(flag) { cout<<"can't join thread and thread has been detached"<<endl; } cout<<"the main thread sleeping for 8 seconds "<<endl; sleep(8); cout<<"the main thread done"<<endl; return 0; } /* answer: can't join thread and thread has been detached the main thread sleeping for 8 seconds the son thread sleeping for 5 seconds the son thread done the main thread done */
分析:采用pthread_detach函数分离一个已经创建的线程,子线程睡眠5秒等待主线程将其分离!,这样导致pthread_join函数取不到子线程结束的状态信息
六.线程结束的四种方法
1.在线程运行函数中直接return(推荐)
在return之后,会清理函数内申请的对象,可以避免线程内存泄漏
2.调用pthread_exit(x)【返回x】,采用函数的形式则不会调用return函数,所以不会调用线程作用域内申请的对象的析构函数,会造成内存泄漏
3.使用同一进程中的其他线程终止线程(被动终止),pthread_cancel函数(使用不当会产生死锁,比如通知一个在等待队列中但是又被取消了的线程)
4.终止该线程所在的进程,调用exit()或者主线程return
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南