目录
练习:利用pthread_create中的参数2 attr 设置分离线程
线程
LWP:轻量级的进程,本质仍是进程(在 Linux 环境下)
进程:独立地址空间,拥有 PCB
线程:有独立的 PCB,但没有独立的地址空间(共享进程的地址空间)
Linux 下: 线程:最小的执行单位
进程:最小分配资源单位,可看成是只有一个线程的进程。
察看线程号: ps –Lf 进程id 查看指定线程的 lwp 号
线程间资源
线程共享资源 | 线程非共享资源 |
文件描述符表 | 线程 id |
每种信号的处理方式 | 处理器现场和栈指针(内核栈) |
当前工作目录 | 独立的栈空间(用户空间栈) |
用户 ID 和组 ID | errno 变量 |
内存地址空间 (.text/.data/.bss/heap/共享库) | 信号屏蔽字 |
全局变量 | 调度优先级 |
Linux 内核线程实现原理
1. 轻量级进程,也有 PCB,创建线程使用的底层函数和进程一样,都是 clone
2. 从内核里看进程和线程是一样的,都有各自不同的 PCB,但是 PCB 中指向内存资源的三级页表是相同的
3. 进程可以蜕变成线程
4. 线程可看做寄存器和栈的集合
5. 在 linux 下,线程最是小的执行单位;进程是最小的分配资源单位
三级映射:进程 PCB --> 页目录(可看成数组,首地址位于 PCB 中) --> 页表 --> 物理页面 --> 内存单元
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是虽然虚拟地址一样,但是,页目录、页表、物理页面各不相同。相同的虚拟地址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
但是!线程不同!两个线程具有各自独立的 PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个 PCB 共享一个地址空间。
实际上,无论是创建进程的 fork,还是创建线程的 pthread_create,底层实现都是调用同一个内核函数 clone。如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。
因此: Linux 内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
线程常用函数
获取线程id pthread_self函数
获取线程id。 线程id是在【进程】地址空间内部,用来标识线程身份的id号,不同进程间线程id可以相同
#include <pthread.h>
pthread_t pthread_self(void);
线程 ID: pthread_t
类型,本质:在 Linux 下为无符号整数(%lu)
创建子线程 pthread_create函数
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
tid:传出参数,表新创建的子线程 id
attr:线程属性。传NULL表使用默认属性。
start_rountn:子线程回调函数。创建成功,ptherad_create函数返回时,该函数会被自动调用。
arg:回调函数的参数。没有的话,传NULL
返回值:
成功:0
失败:errno
线程中检查出错返回
fprintf(stderr, "xxx error: %s\n", strerror(ret));
练习:循环创建多个子线程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
int var = 100;
void *test(void *arg){
int i = (int)arg;
var = 200;
sleep(i);
printf("I'm %dthread: pid = %d,tid = %lu\n",i+1 ,getpid() ,pthread_self());
return NULL;
}
int main(int argc,char*argv[]){
pthread_t thread;
printf("main: pid = %d,tid = %lu\n",getpid(), pthread_self());
int i, ret;
for(i = 0;i<5;i++){
ret = pthread_create(&thread ,NULL ,test ,(void *)i);
if(ret != 0){
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(1);
}
}
sleep(5);
printf("var = %d\n",var);
return 0;
}
若在pthread_create中传入引用(void *)&i,相应地修改回调函数:int i = *((int *)arg)
子线程里更改全局变量后,主线程里也跟着发生变化
单个线程退出 pthread_exit函数
只退出当前线程
#include <pthread.h>
void pthread_exit(void *retval);
参数 retval:退出值。 无退出值时,NULL
区分 exit(); 会导致进程内所有线程全部退出。
return: 返回到调用者那里去。
pthread_exit(): 退出当前线程。
练习:退出指定子线程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
int var = 100;
void *func(void){
pthread_exit(NULL);
return NULL;
}
void *test(void *arg){
int i = (int)arg;
var = 200;
sleep(i);
if(i == 2){
//exit(0);//退出进程
//return NULL;//表示返回到调用者哪里去
func();
}
printf("I'm %dthread: pid = %d,tid = %lu\n",i+1 ,getpid() ,pthread_self());
return NULL;
}
int main(int argc,char*argv[]){
pthread_t thread;
printf("main: pid = %d,tid = %lu\n",getpid(), pthread_self());
int i, ret;
for(i = 0;i<5;i++){
ret = pthread_create(&thread ,NULL ,test ,(void *)i);
if(ret != 0){
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(1);
}
}
printf("var = %d\n",var);
pthread_exit(NULL);
}
回收线程 pthread_join函数
阻塞回收线程
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数 thread: 待回收的线程id
retval:传出参数。 回收的那个线程的退出值。
线程异常终止,值为 PTHREAD_CANCELED。
返回值:成功:0
失败:errno
练习:回收线程并获取子线程返回值
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
struct thrd{
int val;
char str[256];
};
void *tfn(void *arg){
struct thrd *tval;
tval = malloc(sizeof(tval));
tval->val = 100;
strcpy(tval->str,"hello thread");
return (void *)tval;
}
int main(int argc,char* argv[]){
pthread_t tid;
struct thrd *retval;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if(ret < 0){
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(1);
}
int jret = pthread_join(tid, (void **)&retval);
if(jret != 0){
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(1);
}
printf("child thread var :%d, str:%s\n", retval->val, retval->str);
pthread_exit(NULL);
}
杀死线程 pthread_cancel函数
杀死一个线程。 需要到达取消点(保存点)
#include <pthread.h>
int pthread_cancel(pthread_t thread);
参数: thread: 待杀死的线程id
返回值: 成功:0
失败:errno
如果,子线程没有到达取消点, 那么 pthread_cancel 无效。
我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();
成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。
练习:主线程调用pthread_cancel杀死子线程
void *tfn1(void* arg){
printf("thread 1 returning\n");
return (void *)111;
}
void *tfn2(void* arg){
printf("thread 2 returning\n");
return (void *)222;
}
void *tfn3(void* arg){
while(1){
printf("thread 3 I am going to die in 3 seconds ...\n");
sleep(1);
pthread_testcancel();//自己添加取消点
}
return (void *)666;
}
int main(void){
pthread_t tid;
void *tret = NULL;
pthread_create(&tid,NULL,tfn1,NULL);
pthread_join(tid,&tret);
printf("thread 1 exit code = %d\n\n", (int)tret);
pthread_create(&tid,NULL,tfn2,NULL);
pthread_join(tid,&tret);
printf("thread 2 exit code = %d\n\n", (int)tret);
pthread_create(&tid,NULL,tfn3,NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid,&tret);
printf("thread 3 exit code = %d\n\n", (int)tret);
pthread_exit(NULL);
}
分离线程 pthread_detach函数
线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。
设置线程分离,线程终止时,会自动清理PCB,无需回收
#include <pthread.h>
int pthread_detach(pthread_t thread);
参数: thread: 待分离的线程id
返回值: 成功:0
失败:errno
练习:利用pthread_create中的参数2 attr 设置分离线程
设置分离属性。
pthread_attr_t attr;
创建一个线程属性结构体变量
pthread_attr_init(&attr);
初始化线程属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
设置线程属性为 【分离态】
pthread_create(&tid, &attr, tfn, NULL);
借助修改后的 设置线程属性 创建为分离态的新线程
pthread_attr_destroy(&attr);
销毁线程属性
终止线程方式
终止某个线程而不终止整个进程,有三种方法:
1. 从线程主函数 return。这种方法对主控线程不适用,从 main 函数 return 相当于调用 exit。
2. 一个线程可以调用 pthread_cancel 终止同一进程中的另一个线程。
3. 线程可以调用 pthread_exit 终止自己。
线程使用注意事项
1. 主线程退出其他线程不退出,主线程应调用 pthread_exit
2. 避免僵尸线程
pthread_join
pthread_detach
pthread_create 指定分离属性
被 join 线程可能在 join 函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
3. malloc 和 mmap 申请的内存可以被其他线程释放(线程间共享)
4. 应避免在多线程模型中调用 fork 除非,马上 exec,子进程中只有调用 fork 的线程存在,其他线程在子进程中均 pthread_exit
5. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制