基本概念
线程概念:
进程:有独立的 进程地址空间。有独立的pcb。 分配资源的最小单位。
线程:有独立的pcb。没有独立的进程地址空间。 最小单位的执行。
区别:
在于是否共享地址空间。实际上,无论是创建进程的 fork,还是创建线程的
pthread_create,底层实现都是调用同一个内核函数 clone。
如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。
因此:Linux 内核是不区分进程和线程的。只在用户层面上进行区分。
ps -Lf 进程id ---> 线程号。LWP --》cpu 执行的最小单位。
线程共享资源:
1.文件描述符表
2.每种信号的处理方式
3.当前工作目录
4.用户 ID 和组 ID
5.内存地址空间 (.text/.data/.bss/heap/共享库) 共享全局变量(线程间通信)
线程非共享资源
1.线程 id
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)
4.errno 变量
5.信号屏蔽字
6.调度优先级
线程优、缺点
优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便
缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb 不支持 3. 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux 下由于实现方法导致进程、线程差别不是很大
线程控制原语(函数)
pthread_t pthread_self(void); 获取线程id。 线程id是在进程地址空间内部,用来标识线程身份的id号。
其作用,对应进程中 getpid() 函数
返回值:本线程id
检查出错返回方式: 注意与进程区别开
线程中:fprintf(stderr, "xxx error: %s\n", strerror(ret)); pthread_join 直接返回错误号
进程中:perror(const char *str); 内核访问errno全局变量,翻译errno成一个字符串,与传入的字符串拼接
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void *), void *arg);
创建子线程。 其作用,对应进程中 fork() 函数。
参1:传出参数,表新创建的子线程 id
参2:线程属性。传NULL表使用默认属性。
参3:子线程回调函数。创建成功,ptherad_create函数返回时,该函数会被自动调用。
参4:参3的参数。没有的话,传NULL
返回值:成功:0
失败:errno
循环创建N个子线程: (循环释放子线程)
for (i = 0; i < 5; i++)
pthread_create(&tid, NULL, tfn, (void *)i); // 将 int 类型 i, 强转成 void *, 传参。
将 pthread_create 函数参 4 修改为(void *)&i, 将线程主函数内改为 i=*((int *)arg) 是否可以?
不可以,变量i在改变,线程主函数访问i的地址,导致访问的变量i已经更新,不是原来传入的变量
如果变量不改变,可以这样传,但是变量会改变不应该传入变量的地址,应该直接传入变量的值
void pthread_exit(void *retval); 退出当前线程。
retval:退出值。 无退出值时,NULL
exit(); 退出当前进程。 任意一个线程调用了 exit,则整个进程的所有线程都终止
return: 返回到函数调用者那里去。 pthread_join捕获 (正常退出)
pthread_exit(): 退出当前线程。退出值retval, 可以被pthread_join捕获 (异常退出)
int pthread_join(pthread_t thread, void **retval); 阻塞 回收线程。
thread: 待回收的线程id
retval:传出参数。 得到的那个线程的退出值(return ***)。pthread_exit()
线程异常借助,值为 -1。 线程被cancel 返回-1
返回值:成功:0
失败:errno
int pthread_detach(pthread_t thread); 设置线程分离,可以不用pthread_join回收,自动回收
thread: 待分离的线程id
返回值:成功:0
失败:errno
int pthread_cancel(pthread_t thread); 杀死一个线程。 需要到达取消点(保存点)---进入内核
thread: 待杀死的线程id
返回值:成功:0
失败:errno
如果,子线程没有到达取消点, 那么 pthread_cancel 无效。
我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();
成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
struct thrd{
int id;
char name[256];
};
void *tfn(void *arg)
{
int i = (int)arg;
struct thrd *retval;
int *exitval = (int *)malloc(sizeof(int)); //在堆上分配内存
*exitval = 74;
if (i == 3){
pthread_exit((void *)exitval); //传出参数不能是局部变量
}
retval = (struct thrd *)malloc(sizeof (retval));
retval->id = i + 1;
strcpy(retval->name, "hello pthread");
printf ("pthread: I am %dth pthread, id = %d\n", i + 1);
return (void *)retval; // 返回值不能是局部变量
}
int main()
{
int ret;
struct thrd td; pthread_t tid[3];
struct thrd *retval; //自己决定返回什么类型
int *exitval;
for (int i = 0; i < 4; i++){
ret = pthread_create(&tid[i], NULL, tfn, (void *)i);
if (ret != 0){
fprintf(stderr,"pthread create error%s\n", strerror(ret)); // 线程中检查错误
exit(1);
}
}
for (int i = 0; i < 3; i++){ //捕获正常退出值,return
pthread_join(tid[i],(void**)&retval);
printf ("join: I am %dth pthread, id = %d, name = %s\n", i + 1, retval->id, retval->name);
}
pthread_join(tid[3], (void**)&exitval); // 捕获pthread_exit() 退出值
printf ("join: I am 4th pthread, my exit val is %d\n", *exitval);
return 0;
}
线程和进程原语对比
进程 线程
fork pthread_create
exit pthread_exit
wait pthread_join
kill pthread_cancel
getpid pthread_self
线程中设置分离属性
设置分离属性。
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. 主线程退出其他线程不退出,主线程应调用 pthread_exit
2. 避免僵尸线程
pthread_join
pthread_detach
pthread_create 指定分离属性
被 join 线程可能在 join 函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
3. malloc 和 mmap 申请的内存可以被其他线程释放
4. 应避免在多线程模型中调用 fork 除非,马上 exec,子进程中只有调用 fork 的线程存在,其他线程在子进程
中均 pthread_exit
5. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制