2023.9.4
1.概述
进程和线程:linux里的线程也属于进程
- 进程有有自己独立的地址空间,多个线程共用一个地址空间。
- 线程更加节省资源,效率不仅可以保持,而且更高。
- 在一个地址空间中多个线程共享:代码段,堆区,全局数据区,打开的文件(文件描述符)都是线程共享的。
- 线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位。
- 每个进程对应一个虚拟地址空间,一个进程只能抢一个CPU时间片。
- 一个地址空间中可以划分出多个线程,在有效的资源基础上,能够抢更多的CPU时间片。
- CPU的调度和切换:线程的上下文切换比进程快的多。
- 线程更加廉价,启动速度快,退出也快,对系统资源冲击小。
2.创建线程
2.1获取当前线程函数
每一个线程都有一个唯一的线程ID。且ID 的类型为pthread_t,这个ID是一个无符号长整型数,如果想要得到当前线程ID,可以调用如下的函数:
pthread_t pthread_self(void); /*返回当前线程的线程id*/
2.2线程创建函数
在一个进程中调用线程创建函数,就可以得到一个子线程(原本的单进程变成了主线程),和进程不通,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。
pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg)
/*
第一个参数是线程id的地址;
第二个参数是线程的属性,一般情况下默认为NULL;
第三个参数是函数指针(要执行的函数);
第四个参数传递给函数的实参,如果要传递多个数据,则需要定义一个结构体来传递多个数据
*/
2.3代码案例:(主先子后原则)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<string.h>
#include <pthread.h>
void* callback(void* arg)
{
for(int i = 0; i < 5; i++)
{
printf("子线程:%d\n",i);
}
printf("子线程:%ld",pthread_self());
return NULL;
}
int main()
{
pthread_t tid;/*线程的id*/
/*第一个参数是线程id的地址;
第二个参数是线程的属性,一般情况下默认为NULL;
第三个参数是函数指针(要执行的函数);
第四个参数传递给函数的实参,如果要传递多个数据,则需要定义一个结构体来传递多个数据*/
pthread_create(&tid, NULL, callback, NULL);
/*编译的时候加-lpthread*/
for(int i = 0; i < 5; i++)
{
printf("主线程:%d\n",i);
}
printf("主线程:%ld\n",pthread_self());
printf("\n");
return 0;
}
输出为:
或者:
每次运行程序都是不一样的,因为多线程是一个抢时间片的过程,主线程执行完了以后就会释放资源,其他的子线程就不继续执行了,所以我们需要想办法来解决问题,应该先子线程再进行主线程。
第一种方法:主线程先等一会,于是我们在代码中添加sleep(3)
for(int i = 0; i < 5; i++)
{
printf("主线程:%d\n",i);
}
printf("主线程:%ld\n",pthread_self());
sleep(3);
那么主线程抢完资源后陷入沉睡,子线程就会开始抢时间片,并执行。执行完后,主线程睡醒,然后程序结束(主线程比子线程先执行)。结果就会如下。
3.线程退出
编写多线程函数的时候,如果想要让线程退出,但不会导致虚拟内存地址空间的释放(即主线程还会继续运行,但是子线程运行中退出了),可以调用线程库中的线程退出函数,调用后立即退出当前线程,且不会影响其他线程的执行。子线程主线程都可以用。(但一般是用在主线程里,因为子线程结束也不会影响主线程,不会释放虚拟内存地址空间。而用在主线程里,让主线程结束,但是子线程还可以继续执行)
3.1线程退出函数
void pthread_exit(void *retval);
/*参数:线程退出的时候携带的数据,
当前子线程的主线程会得到该数据,如果不需要使用,就指定为NULL*/
/*一般是配合线程回收函数进行使用*/
3.2代码案例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<string.h>
#include <pthread.h>
/*这里如果void callback()的话,下面create函数第三个参数应该为(void*)callback*/
void* callback(void* arg)
{
for(int i = 0; i < 5; i++)
{
printf("子线程:%d\n",i);
}
printf("子线程:%ld",pthread_self());
printf("\n");
return NULL;
}
int main()
{
pthread_t tid;/*线程的id*/
pthread_create(&tid, NULL, callback, NULL);
printf("主线程:%ld\n",pthread_self());
pthread_exit(NULL);
return 0;
}
在主线程添加退出函数,可见主线程执行完毕后,子线程还会被执行。
4.线程回收
线程和进程一样,子线程退出的时候,其资源被主线程回收,而线程回收函数是一个阻塞函数。如果还有子线程在运行,调用该函数就会造成阻塞,子线程退出函数接触阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个则需要循环进行回收。
4.1线程回收函数
pthread_join()
int pthread_join(pthread_t thread, void **retval)
/*
第一个参数是要被回收的子线程
第二个参数是二级指针,指向一级指针的地址,是一个传出参数,存储了pthread_exit()传出
的数据,如果不需要就置NULL
*/
4.2代码案例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<string.h>
#include <pthread.h>
struct Test
{
int num;
int age;
};
void* callback(void* arg)
{
for(int i = 0; i < 5; i++)
{
printf("子线程:%d\n",i);
}
printf("子线程:%ld",pthread_self());
printf("\n");
struct Test t;
t.num = 100;
t.age = 6;
pthread_exit(&t);/*传出参数*/
return NULL;/*退出线程,所以这句不能被执行了*/
}
int main()
{
pthread_t tid;/*线程的id*/
pthread_create(&tid, NULL, callback, NULL);
printf("主线程:%ld\n",pthread_self());
// pthread_join(tid, NULL);/*不接收数据*/
/*这是接收数据*/
void *ptr;/*定义一个指针, ptr指向t 的地址*/
pthread_join(tid, &ptr);/*这里是&t(这也是地址)的地址,两级指针指向一级指针的地址*/
struct Test* pt = (struct Test*)ptr;/*对void类型进行转换*/
printf("num:%d,age:%d\n",pt->num,pt->age);
return 0;
}
发现跟我们想的并不一样。这是为什么呢,因为这个地址是子线程的一块栈内存,一个虚拟地址空间只有一个栈,被多个线程瓜分,然后子线程用完,就会还回去,数据被释放。所以这个t变量在释放后就不复存在了。后面得到的只是一个随机数。
怎么保证是正确的?对了!保证地址不被释放,所以放在全局变量即可!
struct Test
{
int num;
int age;
};
struct Test t;
void* callback(void* arg)
{
for(int i = 0; i < 5; i++)
{
printf("子线程:%d\n",i);
}
printf("子线程:%ld",pthread_self());
printf("\n");
t.num = 100;
t.age = 6;
pthread_exit(&t);/*传出参数*/
return NULL;/*退出线程,所以这句不能被执行了*/
}
这是第一种解决方案,第二种解决方案是使用主线程的栈空间。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<string.h>
#include <pthread.h>
struct Test
{
int num;
int age;
};
void* callback(void* arg)
{
for(int i = 0; i < 5; i++)
{
printf("子线程:%d\n",i);
}
printf("子线程:%ld",pthread_self());
printf("\n");
struct Test *t = (struct Test*)arg;/*转换类型后,指针类型,需要用箭头指了*/
t->num = 100;
t->age = 6;
pthread_exit(t);/*传出参数,此时不再是取地址*/
return NULL;/*退出线程,所以这句不能被执行了*/
}
int main()
{
struct Test t;/*需要把t的地址传到子线程。*/
pthread_t tid;/*线程的id*/
pthread_create(&tid, NULL, callback, &t);
printf("主线程:%ld\n",pthread_self());
// pthread_join(tid, NULL);/*不接收数据*/
/*这是接收数据*/
void *ptr;/*定义一个指针, ptr指向t 的地址*/
pthread_join(tid, &ptr);/*这里是&t(这也是地址)的地址,两级指针指向一级指针的地址*/
printf("num:%d,age:%d\n",t.num,t.age);
return 0;
}
5.线程分离
5.1线程分离函数
某些情况下,程序中主线程也是有自己的业务处理流程,如果主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出,主线程就会一直被阻塞,主要的任务也不能执行。
因此提供了pthread_detach(),调用这个函数之后指定的子线程可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了,线程分离后在主线程中用join函数就回收布道子线程资源了。
int pthread_detach(pthread_t thread);
当然当主线程执行完毕后(并非是线程退出),子线程也活不了,毕竟主线程都寄了,覆巢之下焉有完卵!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<string.h>
#include <pthread.h>
struct Test
{
int num;
int age;
};
void* callback(void* arg)
{
for(int i = 0; i < 5; i++)
{
printf("子线程:%d\n",i);
}
printf("子线程:%ld",pthread_self());
printf("\n");
pthread_exit(t);/*传出参数,此时不再是取地址,因为t就是指针类型,t就是地址*/
return NULL;/*退出线程,所以这句不能被执行了*/
}
int main()
{
struct Test t;/*需要把t的地址传到子线程。*/
pthread_t tid;/*线程的id*/
pthread_create(&tid, NULL, callback, &t);
printf("主线程:%ld\n",pthread_self());
pthread_detach(tid);
pthread_exit(NULL);/*退出不影响子线程*/
return 0;
}
6.取消线程
满足条件就会杀死线程
int pthread_cancel(pthread_t thread);
/*调用成功返回0,失败返回非0*/