在上篇博客线程的概念中,我讲到线程很多优点,但是缺乏合理的访问控制,所以我们就来看一下怎么控制线程吧~
POSIX 线程库
我们知道Linux中并没有真正的线程,CPU眼中看到的只有进程实体,所以线程就成了一个虚无的东西,也就意味着Linux并没有提供一组系统调用管理线程。那既然操作系统没有为我们提供结构,我们就自己实现一组接口来管理线程。
POSIX表示可移植操作系统接口,这里我们介绍的这些函数也遵循POSIX标准,这些函数是一组用户级别的调用,与有关的函数构成了一个完整的系列,大多数名字都是以"pthread_“开头的,而要使用函数库,我们就要引入头文件<pthread.h>,并且这里最重要的一点是我们在链接这些线程函数库时编译时需要加上”-pthread"选项,-l 表示链接了第三方库。
1. 线程创建
参数一:返回线程的id,一个输出型参数
参数二:设置线程的属性,使用nullptr表示默认属性
参数三:让子线程执行函数的地址,函数的名称
参数四:子线程执行函数的参数
返回值:成功返回0,失败返回错误码
我们创建一个线程试试:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *handle(void *arg)
{
while(1)
{
cout<<"i am new thread"<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,handle,(void*)"thread 1");
while(1)
{
cout<<"i am main thread"<<endl;
sleep(2);
}
return 0;
}
查看我们创建的线程:
ps -ajL
-L 选项可以显示出线程的相关信息
- 通过上图我们看到有两个进程的PID相同为8974,即说明这两个不是真的进程。而二者的LWP(light weighted process)不相同,说明这两个是不同的轻量级进程即线程。实际上cpu调度一个任务的单位是lwp,而前面我们讲到进程的调度单位是PID是针对单线程的进程来说的。
- 每一个用户态的线程,在内核中都对应一个调度实体,因此OS要进行管理线程就需要先描述后组织,从而线程也拥有自己的进程描述符(task_struct结构体),线程局部存储和线程栈,而结构体开始的地址就是找到某一个描述线程结构的地址,所以用来唯一标识线程的一个整型变量tid就是这些线程描述的首地址,而LWP是用来调度的ID。
- 注意 :线程和进程不同,进程有父进程的概念。但在线程组里,所有线程都是对等关系。 即同一个进程中的线程称为一个线程组,他们没有层次关系,只有主线程和新线程之分。
线程终止
想要不通过终止进程来终止线程,有以下三种方法:
- 从线程函数return。对于主线程return,就相当于对main函数调用exit。
- 线程可以调用pthread_exit终止自己。
- 一个线程可以调用pthread_cancel函数终止线程组中的其他线程。一般用于主线程取消其他新线程的场景。
这里有一个细节是,当你在主线程中取消子线程时,需要保证子线程在主线程被调度之后被取消,否则将拿不到正确的结果。如果子线程被正确的取消那么join函数中的指针将会指向-1.
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* handle(void *arg)
{
int i=3;
while(i--)
{
cout<<"i am new"<<endl;
}
pthread_exit((void*)2);
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,handle,(void*)"thread 1");
void* status;
pthread_join(tid,&status);
cout<<(int*)status<<endl;
return 0;
}
线程等待
- 主线程等待的原因:
防止内存泄漏
主线程获得新线程的退出状态
保证线程退出顺序的同步性
- 等待函数
参数一是等待线程的ID,参数二指向一个指针,后者指向线程的返回值
函数调用成功返回0,失败返回错误码
- 调用该函数的线程将被挂起等待,直到ID为thread的线程终止。所以线程的终止方法不同时,join得到的终止状态是不同的。
这里有一个线程退出的小细节,join函数的第二个参数目的取到线程退出的退出码,这里仅仅是退出码,不像我们进程等待时那样可以判断是否是信号所终止的,因为线程是进程的一个分支,只要线程异常进程就会退出,所以不可能拿到异常码,仅仅可以拿到退出码
1.如果thread线程通过return返回,retval所指向的单元里存放函数的返回值。
2.如果thread线程因为其他线程调用pthread_cancel函数而异常终止,则retval所指向的单元中存放常量 PTHREADCANLELED。
3.如果thread线程自己调用pthread_exit函数而终止,retval所指向的单元中存放传给exit函数的参数。
4.如果对thread线程的终止状态无兴趣,则直接给第二个参数传NUL即可。
- 测试代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* handle(void* arg)
{
int i=3;
while(i--)
cout<<"i am new"<<endl;
return (void*)1;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,handle,(void*)"thread 1");
void* status;
pthread_join(tid,&status);
cout<<(int*)status<<endl;
return 0;
}
线程ID
- 获取线程ID的方法
- 没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题呢?
- 在Linux内核中,多线程的进程被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是第一个线程即主线程的ID;进程描述的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID。
- 所以实际上,你看到的进程pid对应了test_struct中的tgid,而我们看到的lwp对应的是task_struct中的pid。 那么我们看一下 task_struct 的结构。
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;//主线程
...
struct list_head thread_group;//用来描述一个线程组的链表
...
};
当你在用户态下一次调用gitpid你就要知道,其实系统给你返回了test_struct中的tgid。Linux中也提供了gittid用来返回线程id,但是此系统调用并没有封装起来,不是很方便使用。
线程分离
在默认的情况下,新创建的线程时joinable的,线程退出后,需要对他进行pthread_join操作,否则无法释放资源,从而造成内存泄漏,如果不关心线程的返回值,join是一种负担,所以我们可以选择将子线程分离,自动释放线程资源,线程可以自己将自己分离,也可以让别人帮助其分离。
注意: 一个线程在被分离后就不能够进行等待
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* handle(void* arg)
{
pthread_detach(pthread_self());
int i=3;
while(i--)
cout<<"i am new"<<endl;
pthread_cancel(pthread_self());
return (void*)1;
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,handle,(void*)"thread 1");
void* status;
int ret=pthread_join(tid,&status);
cout<<(int*)status<<":"<<ret<<endl;
return 0;
}
总结:
Linux下使用的线程控制函数其实上是一组用户级别的函数,只要我们能熟练的掌握这些函数的使用,对于控制线程就会很简单。