目录
线程概念
线程是什么?
- 在一个程序中的一个执行路线就叫做线程(thread),更准确的定义就是:线程是一个进程的内部控制序列
- 一切进程至少有一个执行线程(线程是运行在进程内部的)
为什么线程是进程的一条执行流?
答:在linux下线程是一个轻量级进程,一个进程至少有一个进程
,linux下的pcb其实就是线程的描述,是执行程序的最基本单位,所以是一个进程中的执行流
Linux下的pcb是线程
- 说的是Linux操作系统,其他操作系统不一定是这样
- Linux是以进程pcb来模拟的,也就说task_struct其实就是线程,进程就成了线程组
- Linux下线程是轻量级进程,一个进程中的线程共享进程的虚拟地址空间
CPU资源足够时,为什么两个进程能同时运行?
- 进程调度:CPU调度进程时,调度的是pcb
- 因为两个进程有两个pcb,因此在pcb资源足够时,多个进行可以实现并行
进程线程对比
- 现在说的进程,是具有一个或多个线程的线程组
- 进程是操作系统分配资源的一个基本单位(资源竞争的基本单位)
- 线程是CPU调度的一个基本单位(执行程序的最小单位)
线程优点
- 一个进程在有可能会有多个线程,而这些线程共享一个虚拟地址空间
- 因此有时候也说线程是运行在进程中的
- 因此线程间通信变得极为方便
- 创建或销毁一个线程相对于进程来说成本更低(不需要创建额外的虚拟地址空间)
- 线程的调度切换相较于进程也比较低,占用的资源少
- 能充分利用多处理器的可并行数量
线程缺点
- 一个进程中可能有多个进程,而这些线程共享一个虚拟地址空间
- 因为线程间的数据访问变得简单,因此数据的安全访问问题更加突出,要考虑的问题增多,编码难度增加
- 缺乏访问控制,一些系统调用和异常都会针对整个进程,因此当一个线程出现了异常,那么整个进程都会崩溃,以及一些系统调用使用时也要注意
进程的优点
安全,稳定(因为进程的独立性)
共享
- 同一地址空间因此共享Text Semgment,Data Segment,定义函数,全局变量都可以访问
- 文件描述符
- 每种信号的处理方式
- 当前工作目录
- 用户id和进程组id
私有
- 线程ID
- 上下文数据
- 栈
- errno
- 信号屏蔽字
- 调度优先级
线程控制
线程的创建
操作系统并没有提供系统调用来创建线程,但是有人在posix标准库中实现了一套线程控制的接口
因为这套接口是库函数,是用户态创建的线程控制,因此创建的线程也叫做用户线程
一个用户线程对应了一个轻量级进程来进行调度运行
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.//编译时必须加-pthread(链接库)
//成功返回0, 如果失败返回错误码
创建了一个用户线程,并且通过第一个参数返回了一个用户线程的id
这个id数字非常大,其实就是一个地址,是一个指向线程自己的线程地址空间在字整个进程虚拟地址空间中的位置
每一个线程都需要自己的栈区,否则如果所有的线程共用一个栈的话会引起混乱
并且cpu是以pcb调度的,因此线程是CPU调度的基本单位,所以每个线程也应该有自己的上下文数据
来保存CPU调度切换时的数据,(而这些都是在线程地址空间中,每一个线程都有自己的地址空间,它们相对独立,因为线程地址空间是在虚拟地址空间内的)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#include<errno.h>
void* thr_start(void* arg)
{
int num =(int)arg;
while(1)
{
printf("this is thread num :%d\n",num);
sleep(1);
}
}
int main()
{
// int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
// void *(*start_routine) (void *), void *arg);
// 功能:创建一个线程(用户线程)
// thread:用于接收一个用户线程ID
// attr:用于设置线程属性,一般置NULL
// start_routine:线程的入口函数,
// 线程运行的就是这个函数,这个函数退出了,线程也就退出看了 // arg:用于给线程入口函数传递参数
// 返回值:0成功 errno失败
pthread_t tid;
int ret;
ret = pthread_create(&tid,NULL,thr_start,(void*)999);
if(ret != 0)
{
perror("pthread_create");
return -1;
}
while(1)
{
printf("---wjf----\n");
sleep(1);
}
return 0;
}
因为创建的是线程所以只有一个进程信息
线程ID
多线程的进程,又被称为线程组,线程组内在每一个线程在内核中都有一个进程描述符
(task_struct)与之对应,进程描述符结构体中的pid,表面上对应的是进程ID,其实不然
它对应的是线程ID,进程组中可能会有多个线程,所以会有多个pid,用来描述进程的描述符
为tgid,对应的是用户层面的进程ID
用户态 | 系统调用 | 内核进程描述符中对应的结构 |
线程ID | pid_t gettid(void) | pid_t pid |
进程ID | pid_t getpid(void) | pid_t tgid |
用户态所展现pid的其实是tgpid的值,线程没有父子之分都是平级的
也可以把进程理解为主(首)线程(主线程与其他线程)
我们在用户态ps查到的进程信息实际是首线程的信息,在用户态得到的进程ID实际是线程组的tgid
如何查看
ps -efL
//命令中的-L选项会出现如下信息
//LWP : 线程ID,即gettid()系统调用的返回值
//NLWP : 线程组内的线程个数
如何获得线程id
#include <sys/types.h>
pid_t gettid(void);
#include<sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);
#include <pthread.h>
pthread_t pthread_self(void);
函数作用:获得线程自身的ID。pthread_t的类型为unsigned long int,所以在打印的时候要使用%lu方式,否则显示结果出问题。
获取当前调用线程的 thread identifier(标识号).
当然编译还是需要加:
-lpthread
gettid 获取的是内核中真实线程ID,
对于多线程进程来说,每个tid实际是不一样的。
而pthread_self获取的是相对于进程的线程控制块的首地址, 只是用来描述统一进程中的不同线程,
线程终止(退出)
如果需要只终止某个线程而不终止某个进程,有三种方式
- 从线程return,对主线程不适用
- 线程可以调用pthread_exit 终止自己
- 调用pthread_cancel终止同一进程中的另一个线程
pthread_exit
#include <pthread.h>
void pthread_exit(void *retval);
Compile and link with -pthread.
功能:线程终止
参数:指针不能指向一个局部变量
返回值:无返回值(与进程相同,结束时无法返回到调用者)
pthread_exit或者return 返回的指针所指向的内存单元必须是全局或者是用malloc分配的
不能在函数栈上分配,因为其他线程得到这个指针时线程函数已经退出了
谁调用谁退出
pthread_cancel
#include <pthread.h>
int pthread_cancel(pthread_t thread);
Compile and link with -pthread.
功能:取消一个执行中的线程
参数:线程ID
返回值:成功返回0,失败返回错误码
退出指定线程
线程等待
为什么要线程等待?
线程退出,其空间没有被释放,仍然在地址空间内(线程退出也会线程僵尸线程,占用资源不释放,最终造成资源泄漏)
新创建的线程不会复用刚才退出线程的地址空间
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Compile and link with -pthread.
线程有一个属性是处于joinable状态,代表这个线程退出后需要被别人等待
一个线程只有处于joinable状态才能被等待(这个为默认状态)
而等待就是获取线程的返回值,并释放资源
//等待指定的线程并获取返回值
//thread:用于指定等待那个线程
//retval:用于接受线程的返回值
//这是一个演示线程等待获取线程返回值的代码
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
void *thr_start(void *arg)
{
return "wjf\n";
}
int main()
{
pthread_t tid ;
int ret = pthread_create(&tid,NULL,thr_start,NULL);
if(ret != 0)
{
perror("creat");
return -1;
}
char *ptr;
pthread_join(tid,(void**)&ptr);
printf("child thread say :%s\n",ptr);
return 0;
}
调用该函数的线程将挂起等待,直到id为thread的线程终止,该线程以不同方式终止,获得的终止状态是不同的
- 如果线程通过return终止,指针指向的单元里存放的是线程函数的返回值
- 如果线程通过pthread_canel异常终止,指针指向的单元里存放的是常数PTHREAD_CANCELED
- 如果线程通过pthread_exit终止,指针指向的单元放的是传给pthread_exit1的参数
- 如果不关心,可以传NULL
线程分离
我们等待一个线程是因为要获取线程的返回值,并且释放资源,假如不关心返回值,那么这个等待将毫无意义
因此就有了一个线程属性叫:线程分离属性,detach属性,这个属性需要设置,它告诉操作系统
这个线程我不关心返回值,所以直接退出回收资源
而设置一个分离属性我们成为线程分离
detach属性和joinable属性相对。两者不能同时存在
如果一个线程属性为detach,那么调用pthread_jion的时候会直接报错,所以我们说只有pthread_join状态才能被等待
#include <pthread.h>
int pthread_detach(pthread_t thread);
Compile and link with -pthread
//分离指定线程,设置为分离属性