内核中没有明确的线程的概念,线程作为轻量级进程。所以不会提供线程的系统调用,只提供了轻量级进程的系统调用,但这个接口比较复杂,使用很不方便,我们用户,需要一个线程的接口。应用层对轻量级进程的接口进行封装,为用户提供了应用层线程的接口,封装在pthread线程库。几乎所有的linux平台都默认自带这个库,编写多线程代码,需要使用第三方pthread库
目录
1.POSIX线程库
2.创建线程
3.进程ID和线程ID
4.线程id及进程地址空间布局
5.线程终止
6.线程等待
7.测试
8.c++11线程
9.多线程私有变量
10.线程分离
1. POSIX线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread”打头的
要使用这些函数库,需要引入头文件<pthrad.h>
链接这些线程函数库时要使用编译器命令的“lpthread”选项
2. 创建线程
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(start_routine)
(void), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
传统的一些函数是成功返回0,失败返回-1.并且对全局变量errno赋值以指示错误
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做),而是将错误码通过返回值返回,可以调用sterror函数解析
pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码,对于pthreads函数的 错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小
测试
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *run(void *args)
{
while (true)
{
printf("new thread: %d\n", getpid());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, run, nullptr);
while (true)
{
printf("main thread: %d\n", getpid());
sleep(1);
}
}
因为是第三方库,所以编译时需要-l带上pthread库名
g++ -o $@ $^ -std=c++11 -lpthread
pid是一样的,说明是一个进程
3. 进程ID和线程ID
在linux中,目前的线程实现是Native POSIX Thread Library,简称NPTL,在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)
没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程后,情况发生了变化,一个用户机场南管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求 进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题?
linux引入了进程组的概念
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};
多线程的进程,又称为线程组,线程组内的每一个线程在内核中都存在一个进程描述符与之对应。进程描述符结构体中的pid,表面上是对应进程id,起始对应线程ID,进程描述符的tgid,含义是Thread Group ID,对应用户层面的进程id
用户态 | 系统调用 | 内核进程描述符中对应的结构 |
---|---|---|
线程id | pid_t gettid (void) | pid_t pid |
进程id | pid_t getpid (void) | pid_t tgid |
现在介绍的线程ID,不同于pthread_t类型的线程id,和进程id一样,线程ID是pid_t类型,而且是用来唯一标识线程的一个整型变量
查看轻量级进程
ps -aL
ps命令的 -L选项,会显示如下信息:
LWP:线程id,即gettid()系统调用的返回值
NLWP: 线程组内线程的个数
进程id和线程id一样是主线程,内核称为(group leader),也就是进程,内核在创建第一个线程时,会将线程组的id的值设置为第一个线程线程id,group_leader指针指向自身,即主进程的进程描述符。不一样的是子线程
linux提供了gettid调用来返回线程id,可是glibc并没有将该系统调用封装起来,在开放接口来供使用。如果确实需要获得线程id,可以使用如下方法:
#include <sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);
/* 线程组ID等于线程ID,group_leader指向自身 */
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);
至于线程组其他线程的id则由内核负责分配,其线程组id总是和主线程的线程组id一样,无论是主线程直接创建线程,还是创建出来的线程再次创建线程,都是这样
if ( clone_flags & CLONE_THREAD )
p->tgid = current->tgid;
if ( clone_flags & CLONE_THREAD ) {
P->group_lead = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}
强调一点,线程和进程不一样,进程有父进程的概念,在线程组里面,所有的线程都是对等关系
4. 线程id及进程地址空间布局
创建轻量级进程的函数
上面的clone函数需要传入调用的函数,自定义栈空间,flag是要不要地址空间共享等待。一个是参数比较负责,一个是有的内容os不让调用。所以线程库对其封装,产生了上面的一些函数。
线程的概念是库维护的,线程库要加载到内存中。要维护多个线程属性合集,也要进行管理。线程要执行的函数是什么,栈空间在哪,线程id是什么,状态是什么,时间片还有多少等待,库需要维护。线程结构有一个线程控制块,存储用户关心的信息,包含线程的栈,id是什么,os关心的信息,LWP底层指向哪一个执行流,os才能运行。所以是用户级线程。对外返回的线程id实际上可以认为是这个线程结构的地址,如下图
pthread_creat会产生一个线程id,存放在第一个参数指向的地址中。该线程id和前面说的线程id不是一回事
前面的线程id属于进程调度的范畴,因为线程是轻量级进程,是os调度的最小单位,所以需要一个数值来唯一标识线程
pthread_creat函数第一个参数指向一个虚拟内存单元,该内存单元的地址即位新创建线程的现场id,属于NPTL线程库的范畴。现场库的后续操作,就是根据该线程id操作线程的
pthread_t pthread_self (void)
prhread_t是什么类型,取决于实现,对于目前实现的NPTL而言,pthread_t类型的线程id,本质是一个进程地址空间上的一个地址
5. 线程终止
如果需要终止某个线程有三种方法:
1.线程函数内调用return,这种方法对主线程不适用,从main函数return相当于调用exit
2.线程调用pthread_exit终止自己
3.一个线程可以调用pthread_cancel终止同一个进程中的另一个线程
pthread_exit
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit或return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了
pthread_cancel
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
6. 线程等待
为什么
已经退出的线程,空间没有被释放,仍然在地址空间中
创建新的现场不会复用刚才退出线程的地址空间
方法
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程终将被挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的 ,总结如下:
1.如果thread线程通过return返回,value_ptr指向的单元里存放的是thread的返回值
2.如果thread线程被别的线程调用thread_cancle异常终止,value_ptr所指向的单元里存放的是PTHREAD_CANCELED,值为-1
3.如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的是传给pthread_exit的参数
4.如果对thread线程的终止状态不感兴趣,可以传NULL做参数
7. 测试
线程的参数是void*,所以还可以返回结构体之类的结果。
用线程计算两个数之前所有数的求和,通过主线程返回取到结果,并判断结果的可靠性
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
using namespace std;
struct Request
{
Request(int st, int en)
:start(st), end(en)
{}
int start;
int end;
};
struct Response
{
int result;
int exitcode; //结果是否可靠
};
void *sum(void *num)
{
Request* eq = static_cast<Request*>(num);
Response* ret = new Response;
int i = eq->start;
while (i <= eq->end)
{
ret->result += i;
i++;
}
delete eq;
return ret;
}
int main()
{
pthread_t tid;
Request* req = new Request(1, 100);
pthread_create(&tid, nullptr, sum, req);
void *ret;
pthread_join(tid, &ret);
printf("结果:%d\n", ((Response *)ret)->result);
}
用一个类接收两数区间,申请一个类,传入转换为对应类型计算,申请结构类放入返回,最后转换类型输出
如果计算量比较大,可以将一个计算分成几组交给每个线程,父线程将所有结果汇总
8. c++11线程
pthread是原生线程库,c++11对这个库进行了封装,也支持多线程了
编译的时候仍然需要带上库连接
void* run()
{
while (true)
{
printf("子线程\n");
sleep(1);
}
}
int main()
{
thread th(run);
th.join();
return 0;
}
原生线程只能用于linux平台,库的封装基于不同平台会有不同的实现,想跨平台的话得用库函数
线程的完整结构是用户级线程+内核级LWP,所谓用户级还是内核级的区分是线程在哪里实现。linux是用户级线程,用户执行流和内核lwp是1:1的
9. 多线程私有变量
生成多条线程,打印地址
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <thread>
#include <vector>
using namespace std;
void* run(void* num)
{
long n = (long)num;
int cnt = 5;
while (cnt--)
{
printf("第%d条线程,地址:%p\n", n, pthread_self());
sleep(1);
}
}
int main()
{
vector<pthread_t> v;
for (long i = 0; i < 3; i++)
{
pthread_t tid;
pthread_create(&tid, nullptr, run, (void*)i);
v.push_back(tid);
}
sleep(1);
for (size_t i = 0; i < 3; i++)
{
pthread_join(v[i], nullptr);
}
}
每条线程的栈空间都是不一样的。对于全局变量,每个线程都可以访问,这种叫共享资源。如果主线程想要得到某个线程的值,也可以取到,通过全局变量判断是哪条线程赋值,主线程访问全局变量。这里取第1条线程:
线程和线程之间,几乎没有秘密,栈的数据,也可以被其他线程看到并访问
尽量让每个线程的变量独立,不要互相影响
线程的局部存储
怎么让每个线程拥有独立的全局变量,可以前面加__pthread。前面线程的机构里说线程的tcb结构里有线程的局部存储,这部分存储的就是动态库中的这些变量,是编译器提供的编译选项,会给每个线程提供一份
__thread int g_val;
打印一下这个地址观察
每条线程对于这个变量都会有独立的地址,也就是每个线程都拥有私有的变量
局部存储只能定义内置类型,不能修饰自定义类型
局部存储可以保存线程里调用流需要多次读取的值,不需要传入参数或调用函数就可以访问
10. 线程分离
线程的等待是阻塞式的,但如果想让主线程做其他事情,同时不关心次线程的执行结果,可以将线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要join操作,否则无法释放资源造成内存泄露
分离后线程退出会自动释放资源
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程分离,也可以线程自己分离自己。joinable和分离是冲突的,不能既joinable又分离。分离后继续join会失败
detch的分离实际上是修改tcb的属性,记录有没有被分离,如果分离了就不能等待