内核中并没有线程的概念,只有PCB的概念,所以线程并没有系统调用。
但是为了使用户操作线程方便,就有大佬封装了一个POSIX线程库,可以使我们在用户层完成线程的创建销毁、以及其他操作。
POSIX线程库
- 使用时需要包含 <pthread.h> 头文件
- 链接这些线程库时,需要加上编译命令 “-lpthread”选项
创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*star t_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,
attr为NULL表⽰使⽤默认属性
start_routine:是个函数地址,线程启动后要执⾏的函数
//线程的入口函数,线程一创建出来就去执行这个函数,主线程的入口函数是main
arg:传给线程入口函数的参数
返回值:成功返回0;失败返回错误码
代码演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void* ThreadEntry(void* arg)
{
(void)arg;
while(1)
{
printf("New Thread\n");
sleep(1);
}
return NULL;
}
int main()
{
//创建一个线程
pthread_t tid;
pthread_create(&tid,NULL,ThreadEntry,NULL);
//主线程进行死循环
while(1)
{
printf("Main Thread\n");
sleep(1);
}
return 0;
}
线程的执行顺序不一定,由操作系统决定。
利用 pshack [进程id] 显示当前进程中线程的调用炸
LWP 45128 : 作用在用户态,帮助用户控制线程
0x00007fxxxxxx :作用在内核态,协助调度
利用gdb调试线程
使用 gdb attach [进程id]
info thread 查看所有线程
以上 1,2的编号表示现在有线程的编号,*表示当前线程,这时用bt查看调用栈就是1的调用栈
thread [编号] 切换线程
线程终止
- 从线程 return,如果是从主线程return,相当于exit,所有线程都结束
- 线程可调用 pthread_exit终止自己
- 一个线程可以调用 pthread_cancel终止同一进程中的另一进程(参数传 pthread_self()时终止自己)
//线程终止
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向⼀个局部变量,线层入口函数的返回值(基本不用)。
返回值:⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)
//取消一个执行中的线程
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
线程等待与分离
线程等待
为什么要线程等待?
- 线程结束以后会有返回结果(线程入口函数返回的void*),我们需要读取返回结果,回收资源,避免类似于僵尸进程的效果,这里可理解为内存泄漏
进程等待和线程等待
- 进程等待只可以父进程等子进程,一个线程则可以等待同组的任何一个线程
- 进程等待(阻塞、非阻塞),线程等待只有阻塞等待
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向⼀个指针,后者指向线程的返回值
一般使用pthread_join来保证线程的结束顺序
返回值:成功返回0;失败返回错误码
代码演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void* ThreadEntry1(void* arg)
{
(void)arg;
printf("Thread1\n");
return (void*)1;
}
void* ThreadEntry2(void* arg)
{
(void)arg;
printf("Thread2\n");
pthread_exit((void*)2);
return NULL;
}
void* ThreadEntry3(void* arg)
{
(void)arg;
//终止自己
printf("Thread3\n");
pthread_cancel(pthread_self());
return NULL;
}
int main()
{
pthread_t t1,t2,t3;
pthread_create(&t1,NULL,ThreadEntry1,NULL);
pthread_create(&t2,NULL,ThreadEntry2,NULL);
pthread_create(&t3,NULL,ThreadEntry3,NULL);
void* ret = NULL;
pthread_join(t1,&ret);//调用该函数的线程会挂起等待,直到t1线程终止
printf("Thread1 = %p\n",ret);
pthread_join(t2,&ret);
printf("Thread2 = %p\n",ret);
pthread_join(t3,&ret);
printf("Thread3 = %p\n",ret);
//3次pthread_join是串行的,必须t1结束以后,对1回收完毕以后才会调用2
return 0;
}
在学习进程的时候我们也说进程父子进程的执行顺序不一定,但是我们却很难看到子进程先于父进程执行。
那为什么我们在这里很容易可以看到线程执行顺序不一定呢?
其实进程的执行顺序确实是不一定的,但是每次创建一个进程的代价比较大,所以父进程就有很大的概率去执行,而线程的创建代价比较小,所以很容易看到执行顺序不一定这个结果。
但是线层等待的顺序一定是顺序的,可以认为三个等待函数时串行执行的,必须前面的执行完后面的才可以执行。
代码验证线程共享栈、堆全局区的情况
线程分离
- 在默认情况下创建的线程是joinable的,线程退出后,要对其进行pthread_join操作,否则无法回收资源,造成内存泄漏。
- 如果不关心线程的返回值,那么join就是一个负担,这个是后可以告诉系统线程退出时,由操作系统自动回收资源。
int pthread_detach(pthread_t tid);
线程也可以对自己进行线程分离
pthread_detach(pthread——self());
joinable和线程分离是两个对立的状态,两者不可能同时存在。