前言
本文介绍了Linux下的线程控制。
一、POSIX线程库
1.概念
与线程有关的函数构成了一个完整的系列,大多数函数名都是以“pthread_”为开头的,要使用这些函数需要引入头文件pthread.h
。链接这些线程函数库需要使用编译器命令的-lpthread
选项。
2.pthread线程库是应用层的原生线程库
我们在Linux之多线程(上)这篇文章中了解:在Linux中没有真正意义上的线程,因此系统无法直接给我们提供创建线程的系统接口,只能提供创建轻量级进程额度接口。
在用户角度,当我们想创建一个线程时会想使用thread_sreate这样的接口,而不是像vfork这样的函数。用户不能直接访问OS,所以OS在用户和系统调用之间提供了编写好的用户级线程库,这个库一般称为pthread库。任何Linux操作下系统都必须默认携带这个库,因此这个库也称为原生线程库。
原生线程库本质上是对轻量级进程的系统调用(clone)做了封装——pthread_create,用户层也因此模拟实现了一套线程相关的接口。
用户眼中的线程实际上会在OS内部被转化为轻量级进程。
3.错误的检查
传统的函数,成功就返回0,失败返回-1,并且给全局变量errno赋错误码以指示错误。
pthread函数出错时并不会设置全局变量errno(大部分其他POSIX函数会设置),而是讲错误码通过返回值返回。当然,pthread函数是提供了线程内的errno变量,以支持其他使用errno的代码。对于pthread函数的错误,建议通过返回值判定,因为读取返回值比读取线程内的errno变量的开销更小。
二、线程控制
1.创建线程——pthread_create
pthread_create函数
参数:
- thread:获取线程的ID,该参数是输出型参数;
- attr:用于设置创建线程的属性,传入nullptr表示默认,这个属性一般不用管直接传nullptr就行;
- start_routine:函数地址,表示线程启动后要执行的函数;
- arg:传给线程例程的参数。
返回值:
成功返回0,失败返回错误码。
例子
创建一个新线程
文件mythread.cc
1 #include<iostream> 2 #include<string> 3 #include<unistd.h> 4 #include<pthread.h> 5 #include<assert.h> 6 using namespace std; 7 void* thread_routine(void* args) 8 { 9 string name = static_cast<const char*>(args);//安全的进行强制类型转换 10 while(1) 11 { 12 cout<<"这是新线程, name:"<<name<<endl; 13 sleep(1); 14 } 15 } 16 int main() 17 { 18 pthread_t id; 19 int n = pthread_create(&id, nullptr, thread_routine, (void*)"thread new"); 20 assert(n == 0); 21 (void)n; 22 while(1) 23 { 24 cout<<"我是主线程,我正在运行"<<endl; 25 sleep(1); 26 } 27 return 0; 28 }
这里编译运行需要注意的是,pthread_create接口是库提供给我们的,我们使用的接口如果不是语言上的接口或者操作系统的接口,而是库提供的接口,那么在编译的时候是无法通过的,需要链接这个库才能编译成功。要链接这个库首先要找到这个库,-L:找到库在哪里;-l:找到头文件在哪里,库已经在系统中安装好了,所以除了高所系统库和头文件在哪里以外,还要知道是链接哪一个库(库名字)。
所以要加上-lpthread
。
此时我们用ps axj命令查看当前进程的信息时,虽然此时该进程中有两个线程,但是我们只能看到一个进程,因为这两个线程是属于一个进程的:
要想查看到轻量级进程需要使用ps -aL指令:
其中LWP(Light Weight Process)表示的是轻量级进程的ID,可以看到显示出的两个轻量级进程的PID是相同的(因为它们属于同一个进程),而每个轻量级进程都有唯一的LWP。
注意:主线程的PID和LWP是相同的,PID和LWP不相同的是新线程,所以CPU进行调度时,是以LWP为标识符进行标定一个线程执行流。
线程一旦被创建,几乎所有的资源都是被所有线程所共享的,因此线程之间想要进行交互是很容易的,因为直接就可以看到同一份资源。
线程要有自己的私有资源:
线程被调度就要有独立的PCB属性——LWP;
线程被切换时正在运行,需要进行上下文的保存,因此线程要有私有的上下文结构;
每个线程都要独立的运行,所以线程要有自己独立额度栈结构。
主线程创建一批新线程
让主线程一次性创建十个新线程,并让创建的每个新线程都去执行start_routine
函数,即start_routine
这个函数会被重复进入。并且start_routine
函数是可重入函数(不会产生二义性),没有因为一个线程去影响另一个线程。在函数定义内定义的变量都是局部变量具有临时性,所以在多线程的情况下也没有问题。
文件mythread.cc
这也说明了每个线程都有自己独立的栈结构。
2.获取线程ID——pthread_self
获取线程ID:1.创建线程时通过输出型参数获取;2.通过pthread_self接口函数获取。
我们可以通过主线程打印出新线程的ID,再通过新线程打印出自己的ID,判断是否相同。
结果是相同的。
3.线程等待——pthread_join
一个线程退出时和进程一样是需要等待的,如果线程不等待,对应的PCB没有被释放也会造成类似僵尸进程的问题(内存泄漏)。所以线程也需要被等待:1.获取新线程的退出信息;2.回收新线程对应的PCB等内核资源,防止内存泄漏。
参数:
thread是被都能打线程的ID;
retval:线程退出时的退出码。
void** retval:输出型参数,主要用于获取线程退出时返回的退出结果。之所以是void**,是因为如果想作为输出型结果返回就必须是void**(因为线程函数的返回结果是void*)
返回值:线程等待成功返回0,等待失败返回错误码。
没有看到线程退出时对应的退出码是因为线程出异常时收到信号,整个进程都会退出,而退出信息需要进程来关心,所以pthread_join默认会认为函数是调用成功的(等待成功),它不会考虑程序出现异常的情况,异常问题是进程该考虑的情况。
4.线程终止——return、pthread_exit、pthread_cancel
一个线程,如果只是想终止该线程而不是整个进程,有三种做法:
- 直接从线程的函数结束,return就可以终止该线程;
- 线程可以自己调用pthread_exit终止自己;
- 一个线程可以调用pthread_cancel来终止同一个进程中的另一个线程。
return
pthread_exit
pthread_cancel
5.分离线程——pthread_detach
线程是可以等待的,等待的时候是join的等待(阻塞式等待)。如果我们不想等待:不去等待线程,而是进行分离线程处理。默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放该线程的资源,造成内存泄漏。
如果我们并不关心线程的返回值,此时join对我们来说是一种负担,这时,我们可以告诉OS,当线程退出时,自动释放线程资源,这种策略就是线程分离。
pthread_detach函数
例子
创建新线程,让主线程与新线程运行起来,主线程等待新线程退出,等待完毕返回n。由于我们现在让新线程进行分离,那么按照理论此时主线程的等待结果是失败的。
文件mythread.cc
1 #include<iostream> 2 #include<unistd.h> 3 #include<pthread.h> 4 #include<string.h> 5 using namespace std; 6 #include<string> 7 string changeld(const pthread_t& thread_id) 8 { 9 char tid[128]; 10 snprintf(tid, sizeof(tid), "0x%x", thread_id); 11 return tid; 12 } 13 void* start_routine(void* args) 14 { 15 string name = static_cast<const char*>(args); 16 pthread_detach(pthread_self());//线程分离,设置为分离状态 17 int cnt = 5; 18 while(cnt--) 19 { 20 cout<<name<<"is running..."<<changeld(pthread_self())<<endl; 21 sleep(1); 22 } 23 return nullptr; 24 } 25 int main() 26 { 27 pthread_t tid; 28 pthread_create(&tid, nullptr, start_routine, (void*)"thread 1"); 29 string main_id = changeld(pthread_self()); 30 cout<<"main thread running... new thread id:"<<changeld(tid)<<"main thread id:"<<main_id<<endl; 31 int n = pthread_join(tid, nullptr); 32 cout<<"result:"<<n<<":"<<strerror(n)<<endl; 33 return 0; 34 }
运行:
但是我们发现等待结果依旧是成功的,这是为什么?
因为,我们创建新线程后,并不确定新线程和主线程哪个先被调度,所以可能导致我们还没有执行新线程的pthread_detach时,主线程就去等待新线程了。也就是说,新线程还没有来得及分离自己,主线程就去等待了。
因此我们可以让主线程sleep(2),保证新线程是分离的状态主线程再去等待,则此时等待是失败的。
文件mythread.c
1 #include<iostream> 2 #include<unistd.h> 3 #include<pthread.h> 4 #include<string.h> 5 using namespace std; 6 #include<string> 7 string changeld(const pthread_t& thread_id) 8 { 9 char tid[128]; 10 snprintf(tid, sizeof(tid), "0x%x", thread_id); 11 return tid; 12 } 13 void* start_routine(void* args) 14 { 15 string name = static_cast<const char*>(args); 16 pthread_detach(pthread_self());//线程分离,设置为分离状态 17 int cnt = 5; 18 while(cnt--) 19 { 20 cout<<name<<"is running..."<<changeld(pthread_self())<<endl; 21 sleep(1); 22 } 23 return nullptr; 24 } 25 int main() 26 { 27 pthread_t tid; 28 pthread_create(&tid, nullptr, start_routine, (void*)"thread 1"); 29 string main_id = changeld(pthread_self()); 30 cout<<"main thread running... new thread id:"<<changeld(tid)<<"main thread id:"<<main_id<<endl; 31 sleep(2); 32 int n = pthread_join(tid, nullptr); 33 cout<<"result:"<<n<<":"<<strerror(n)<<endl; 34 return 0; 35 }
运行:
当然,我们也可以直接让主线程直接pthread_detach,而不是让新线程分离:线程运行起来就直接分离了,分离成功就去join了,此时新线程就去等待了。
文件mythread.c
1 #include<iostream> 2 #include<unistd.h> 3 #include<pthread.h> 4 #include<string.h> 5 using namespace std; 6 #include<string> 7 string changeld(const pthread_t& thread_id) 8 { 9 char tid[128]; 10 snprintf(tid, sizeof(tid), "0x%x", thread_id); 11 return tid; 12 } 13 void* start_routine(void* args) 14 { 15 string name = static_cast<const char*>(args); 16 int cnt = 5; 17 while(cnt--) 18 { 19 cout<<name<<"is running..."<<changeld(pthread_self())<<endl; 20 sleep(1); 21 } 22 return nullptr; 23 } 24 int main() 25 { 26 pthread_t tid; 27 pthread_create(&tid, nullptr, start_routine, (void*)"thread 1"); 28 string main_id = changeld(pthread_self()); 29 pthread_detach(tid);//让主线程线程分离 30 cout<<"main thread running... new thread id:"<<changeld(tid)<<"main thread id:"<<main_id<<endl; 31 int n = pthread_join(tid, nullptr); 32 cout<<"result:"<<n<<":"<<strerror(n)<<endl; 33 return 0; 34 }
总结
以上就是今天要讲的内容,本文介绍了线程控制相关的概念。本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!