线程基础
1、为什么要用线程
在
Linux
创建的初期,内核一直就没有实现“线程”这个东西。每个用户进程有自己的地址空间。
系统为每个用户进程创建一个
task_struct
来描述该进程;该结构体中包含了一个指针指向该进程的虚拟地址空间映射表。
实际上
task_struct
和地址空间映射表一起用来表示一个进程:
![](https://img-blog.csdnimg.cn/22d35117a6f946c7b3392d23ede1833c.png)
由于进程的地址空间是私有的,因此在进程间上下文切换时,系统开销比较大;为了提高系统的性能,许多操作系统规范里引入了轻量级进程的概念,也被称为线程。
在同一个进程中创建的线程共享该进程的地址空间。
Linux
里同样用
task_struct
来描述一个线程。线程和进程都参与统一的调度。
![](https://img-blog.csdnimg.cn/cdc812ffd18b4e11b0429b94e9f9ef2a.png)
很多个
task_struct
代表一个线程
(
内核管理线程
)
紫色内存映射表,通常线程指的是共享相同地址空间的多个任务。
使用多线程的好处:
1、大大提高了任务切换的效率;
2、避免了额外的
TLB & cache
的刷新;
3、多线程编程使用的是一个线程库
pthread,
编译的时候要给
gcc
加上
-lpthread;
4、同样情况下: 多线程的资源消耗要小于多进程, 特别是在一些手持设备中,这个表现的比较明显。
CPU 和内存之间的缓存
(cache)
中有一个部件
TLB
(存储当前预取的内存中就近的数据)。当进程切换时候,不同进程的 VMA 完全不同,所有整个
TLB
里面缓冲的数据就失效,要重新刷新 ;当线程切换的时候,由于同一进程中的所有线程都是共享同一 VMA(
虚拟内存入口
),
就只要部分刷新
TLB。
多线程编程中,要少用慎用全局变量,如果非要使用全局变量或全局结构体变量,则要考虑是否要加资源保护机制,比如:信号量、互斥锁。进程和线程都是解决问题的方式。
线程特点
1、CPU
调度的最小单位;
2、
线程从属于进程;
3、
在同一个进程中创建的线程共享该进程的地址空间,线程和进程统一调度;
4.
线程在程序中以函数的形式体现,一个程序就会产生进程,
产生
4G
空间;线程在栈里面是不同的,线程的空间叫栈帧,每个线程的栈帧是私有的。
![](https://img-blog.csdnimg.cn/50f2aca226fe49d995a8b23c5338e14f.png)
多线程编程
NPTL
(
Native POSIX Thread Library
)
线程库中提供了如下基本操作 :
![](https://img-blog.csdnimg.cn/6dc6f80aa80f4a1b8035651b54eff869.png)
创建线程 pthread_create 函数
案例打印线程的 id 号
问题:运行时线程代码没有打印输出为什么? 如果子线程后结束。因为 main 所在代码运行结束 return 时会调用 exit()导致进程结束,子线程强制结束了。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <stdlib.h>
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/syscall.h> /* For SYS_xxx definitions */
void *fun(void *n)
{
unsigned long sys_tid=syscall(SYS_gettid); //和 ps 看到线程号一致
unsigned long pthread_tid=pthread_self();
printf("我是子线程!\n");
printf("pthread_self():%lu,gettid():%lu\n",pthread_tid,sys_tid);
sleep(50);
return (void *)0;
}
int main(void)
{
pthread_t tid;
int err;
if((err=pthread_create(&tid,NULL,fun,NULL))!=0){
fprintf(stderr, " pthread_create: %s\n",strerror(err));
exit(1);
}
printf("this is main\n");
printf("tid=%lu\n",tid);
sleep(60);//必须加否则子线程不能打印
return 0;
}
在多线程中,
pthread_self()
函数获得的线程号是
pthread
库对线程的编号,而不是
Linux
系统对线程的编号。
pthread_create()
返回的线程号,使用
top
命令是查不到的,
top
显示的是
Linux
的线程号。
在单线程中,
Linux
的线程号和进程号是一样的。
在多线程中,主线程的线程号(
main
函数的线程)与进程号一样,其他线程则有各自的线程号。
与
getpid()
函数不同的是,
Linux
并没有直接给一个
gettid()
的
API
,而是使用
syscall()
直接用
SYS_gettid
的系统调用号去获取线程号。
在
top -H -p
时显示的
tid
,与
syscall(SYS_gettid
)返回的一致,与 pthread_self
返回的不一致,后者的与
pthread_create
获得的一致。
查看
pid
创建的所有线程 :
ps -T -p pid
多线程等待回收子线程函数
pthread_exit 函数
只退出当前子线程,同时给父线程待会退出的状态和值。
pthread_cancel 函数
参数 thread 是要取消的目标线程的线程 ID。该函数并不阻塞调用线程,它发出取消请求后就返回了。类比于进程的 kill 函数,但是不同的是,调用函数时,并非一定能取消掉该线程,因为这个函数需要线程进到内核时才会被杀掉,所以线程如果一直运行于用户空间,就没有契机进入内核也就无法取消(例 while(1){}空循环),此时可以使用 pthread_testcancel();进入内核。
如果成功,
pthread_cancel
返回
0
,如果不成功,
pthread_cancel
返回一个非零的错误码。
https://zhuanlan.zhihu.com/p/175943809
线程生命周期
设置线程的分离属性
int pthread_detach(pthread_t thread);
参数: 设置哪个线程要分离
子线程不需要父线程使用
pthread_join() 清理子线程残留的东西;使用此函数后,会被自动清理,
无需回收。分离之后再使用 pthread_join
进行回收会报错。
没有分离的线程,
它的资源不会释放,需要调用
pthread_join
回收,
进程结束后占用的资源全部释放。
下面是一个示例:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *mypthreadFunction(void *pvoid)
{
int i = 0;
while(1)
{
printf("thread function, i: %d\n", i++);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, mypthreadFunction, NULL);
pthread_detach(tid); // 将线程分离
//2秒钟之后取消线程
sleep(2);
pthread_cancel(tid);
printf("had cancel the thread!\n");
return 0;
}