线程
为什么有了进程还需要线程
进程切换的时候会花费很大的代价
(1)上下文切换,CPU寄存器需要切换
(2)虚拟地址和物理地址的映射需要切换
进程间通信麻烦
线程是轻量级的进程
(1)线程是一个正在执行的程序,但是它不在是资源分配的最小单位
(2)同一个进程存在多个线程,多个线程共享内存资源
(3)线程也有上下文状态(主要的是PC指针
和stack(栈)指针
)
用户级线程
:不能够被CPU感知到的CPU不能根据用户使用线程的多少来进行调度,用户自己分配这个进程内部各个CPU使用情况,线程调度由进程处理
内核级线程
:线程调用是由操作系统处理
CPU调度以线程为单位
原来的进程都是当线程进程
在Linux操作系统中,进程控制块和线程控制块合二为一了,每个进程和线程都有task_struct
的描述
引入线程的好处
(1)减少了上下文切换的代价
(2)消灭了页表切换
(3)线程是共享内存的可以无痛通信
创建线程
当我们启动进程就会自动创建一个主线程,主线程栈区从main
函数开始压栈
在主线程中可以使用pthread_create
函数创建一个子线程
pthread_t *thread
:线程ID,不同操作系统中pthread的实现是不一样的
pthread_attr *attr
:线程的属性,填NULL表示我们使用默认属性
void *(*start routine) (void *)
:线程启动函数,参数和返回值都是void*
类型-----void *func(void*)
子线程的main
函数
void *arg
:传递给start_routine
的参数
获取自己线程的线程ID
Makefile
文件后面添加-pthread
让main
主线程创建一个子线程,让主线程和子线程输出自己的线程ID
如果我们让父线程sleep(1)
,最终只会打印main
主线程的线程ID
,因为当主线程main
,也就是进程终止了,那么这个进程里面的线程也就无法运行了
如果我们让主线程只睡20微秒,那么我们打印输出的记录有可能由三条记录,也有可能只有两条记录,因为数据从输入到展示在命令框要经过三个步骤,首先是printf
将数据拷贝到stdout
,stdout
将数据拷贝(原子操作)到内核的文件对象,然后再清空我们的stdout
,因此主线程有可能在清空stdout
之前就终止,这时,命令框已经输出一次数据,然后发现stdout
的数据还存在,就会把stdout
的数据在刷新一遍在显示框,这就出现打印三次结果
多线程下不能使用perror
一个典型的报错会做两件事
(1)return -1
(2)修改全局变量errno
然后perror
会根据errno
生成错误提示字符串,所以perror
依赖的数据就会存储在数据段里面,但是多个和线程都可以同时共同访问一个数据段,但是此时如果由多个进程报错,其他线程报错信息会覆盖本线程的包i错信息,因此就不能获取到正确的错误信息
在多线程中报错不会返回-1
,而是返回数值,通过返回值数值来确定报错的类型strerror可以通过传入的数值返回给我们一个错误提示字符串
唯一的坏处是它不能打印这个字符串,因此我们需要通过fprintf(stderr)
来将错误信息输出
检测我们的进程能够创建多少线程
多线程共享内存空间
多线程可以共享同一个数据段
多线程共享堆空间,主线程和子线程使用同一个数值的地址使用pthread_create
传递地址参数,其实我们是直接把第四个参数拷贝到另外一个线程的栈帧里面
多线程之间传递一个整数,直接传递一个long
类型的数据,因为void *
是8个字节,long
类型也是8个字节,不会有信息丢失,如果我们希望主线程和子线程之间共享内存那么就传递指针,如果不希望共享内存你那么久传递long
类型。void *
既可以当指针
用也可以当long
来用
多线程的栈区是相对独立的,一个线程可以通过地址区访问另一个线程的栈区
线程的终止
一个进程中的任意一个线程只要触发其中任意一个信号,那么整个进程就会终止,其中所有的线程也都会终止
(1)
main
线程的return
函数、(2)exit
命令、(3)_exit/_Exit
、(4)abort
、(5)收到导致进程终止的信号
子线程终止自己
(1)从threadFunc
中return
(尽量不要用)
(2)pthread_exit
(主线程不要调用)
void *retval
子线程的返回值
pthread_join回收线程的资源
join
等待任何一个线程的终止
pthread_t thread
目标线程的tid
(不是一个指针)
void **retval
是拷贝子线程的终止状态,主调函数中申请void *
的内存join
试图修改主调函数中的void *
join和exit的例子
pthread_join的的错误用法
多线程和信号不能同时使用
多线程会共享注册信号的信息,用户无法得知是哪个线程递送信号
线程的取消类似于信号:线程可以在运行过程中给别的线程发送一个取消请求,另一个线程收到取消请求之后,他不会立刻终止,他会将自己的取消标志置为1,运行到一些特殊函数的时候就会取消,在一些特殊函数之前或者调用之后就会取消进程,这些特殊的函数称之为取消点
取消点函数
(1)操作文件的系统调用、(2)可能引发阻塞的系统调用;库函数和系统调用都是取消点函数
pthread_mutex_lock
加锁不是取消点
如果线程是被pthread_cacel
终止的并且取消成功,那么终止的线程返回值为-1
但是如果线程没有碰到取消点函数那么也就不会执行终止指令,会继续执行下去
ps -elLf|grep pthread_cancel
查看线程运行状态
手动增加取消点
pthread_testcancel
如果取消标志位为真,就终止本线程
异步终止可能会导致资源泄漏,因为malloc
和free
函数都是取消点,我们不知道pthrread_cancel
函数实在malloc
之前还是free
之后或者是在malloc
和free
之间,这就很容易造成资源泄漏。
因此在目标线程运行到取消点函数和取消点函数调用完前终止线程之间会调用线程中终止清理函数
资源清理栈(自动根据申请了多少资源,就释放多少资源)
当我们申请资源malloc ; open/fopen/opendir ; semop/mutex_lok
对应的释放行为 free ; close/fclose/closedir ; semop/mutex_unlock
,
我们会去维护一个特殊的结构,资源清理栈,里面存储了资源释放的行为,当我们申请资源之后就会把对应的释放行为压栈(pthread_cleanup_push
)
当线程因为(1)pthread_exit
(不包括在启动函数中return
)、(2)被cancel
终止时;将栈清空
线程可以主动调用pthread_cleanup_pop
释放资源