线程的应用
一、线程的创建:
1. 代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>//p表示posix线程库
void*rout(void *arg)
{
(void)arg;//解决编译器的警告(没有使用的参数),不想用这个参数。
while(1){
printf("I am thread\n");
sleep(1);
}
}
intmain(void)
{
pthread_t tid;
int ret;
if((ret=pthread_create(&tid,NULL,rout,NULL)!=0)){
fprintf(stderr,"pthread_create:%s\n",strerror(ret));
exit(EXIT_FAILURE);
}
while(1){
printf("I am main thread\n");
sleep(1);
}
return 0;
}
主线程里面创建了另外一个线程,运行之后,可以看到两个结果
发生错误:
:
2. 错误原因:需要加入命令:gcc Pthread.c -oPthread -l pthread
结果为:
查看线程的方式:
(1)ps
(2)pstack可以查看线程详细信息
(3)gdb也可以查看线程详细信息,讲gdb附加进程上(附加:把物理地址创建好,把虚拟地址空间和物理地址空间关联到一起gdb已经跑起来了,让gdb和进程关联到一起):gdb attach pid即课把pid附加到进程上【info thread查看线程信息;bt查看主线程的调用栈;thread 数字:切换调用栈进行调试
二、线程终止:
1. 代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
void*rout(void *arg)
{
(void) arg;
int count=0;
while(1){
++count;
if(count>=10)
return;
//pthred_exit(NULL);
printf("I am thread\n");
sleep(1);
}
}
int main(void)
{
pthread_t tid;
int ret;
if((ret=pthread_create(&tid,NULL,rout,NULL)!=0)){
fprintf(stderr,"pthread_create:%s\n",strerror(ret));
exit(EXIT_FAILURE);
}
while(1){
printf("I am main thread%lx\n\n",pthread_self());//打印当前线程的线程id
sleep(1);
}
}
//ctrl+s可能让打印的结果暂停;gdb attach
说明了线程执行的先后顺序是不确定的
法2
//void *rout(void *arg)
//{
// while(1){
// printf("I am thread\n");
// sleep(1);
// }
//}
//int main()
//{
// pthread_t tid;
// int ret;
// if((ret=pthread_create(&tid,NULL,rout,NULL)!=0)){
// fprintf(stderr,"pthread_create:%s\n",strerror(ret));
// exit(EXIT_FAILURE);
// }
//while(1){
// ++count;
// if(count>=10)
// pthread_cancel(tid);
// printf("I am mainthread %lx\n\n",pthread_self());
// sleep(1);
// }
//}
交替打印到一定次数后,调用pthread_cancel函数,把对应的线程终止掉。
结果:
综上:非常不推荐使用pthread_cancel函数,另外一个线程执行到哪个位置是不确定的,若是正在修改bug,直接被终止,会导致程序出现错误。第一种:return最简单、直观。
三、进程等待
1. 代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
void*rout(void *arg)
{
int ret=100;
int count=0;
while(1){
++count;
if(count>=10)
return (void *)ret;//只要void*表示的内存范围可以涵盖int则是可行的
若是超过了void*能够表示的范围可以在堆上创建内存,从而得到一个指针
printf("I am thread\n");
sleep(1);
}
}
intmain(void)
{
pthread_t tid;
pthread_create(&tid,NULL,rout,NULL);
void *ret;
pthread_join(tid,&ret);
printf("ret=%d\n",(int)ret);
return 0;
}
2. 结果
若是超过了void*能够表示的范围可以在堆上创建内存,从而得到一个指针:指针的大小是相同的
1、代码
typedef struct student{
int id;
int score;
int classno;
}student;
void*rout(void *arg)
{
(void)arg;
student* ret=(student*)malloc (sizeof(student));
ret->id=10;
ret->score=100;
ret->classno=30;
int count=0;
while(1){
++count;
if(count>=10)
return (void *)ret;//只要void*表示的内存范围可以涵盖int则是可行的
若是超过了void*能够表示的范围可以在堆上创建内存,从而得到一个指针;void*根据需要想要传什么就传什么,只告诉你地址,不告诉解析地址的方式;如何解析取决于构造时,按照什么样的方式进行构造。构造完成后,从主线程中拿到后进行强转,这个来回强转的过程必须保证方式是一样的。
构造数据时约定怎么解析,使用数据时就怎么解析;两者如果不匹配,就达不到想要的效果。
printf("I am thread\n");
sleep(1);
}
}
intmain(void)
{
pthread_t tid;
pthread_create(&tid,NULL,rout,NULL);
void *ret;
pthread_join(tid,&ret);
student* st=(student*)ret;
printf("ret=%d %d %d\n",st->id,st->score,st->classno);
return 0;
}
四、进程分离
1. 代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
void*rout(void *arg)
{
int ret=100;
int count=0;
while(1){
++count;
if(count>=10)
return (void *)ret;
printf("I am thread\n");
sleep(1);
}
}
intmain(void)
{
pthread_t tid;
pthread_create(&tid,NULL,rout,NULL);
pthread_detach(tid);//通过主线程支持新线程进行分离
while(1){
printf("I am main%lx\n",pthread_self());
sleep(1);
}
return 0;
}
2. 结果
五、线程共用同一虚拟地址空间:同一个线程共享所有的虚拟地址(栈,堆,全局变量,,,,)
1.、全局变量:
(1)代码
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<pthread.h>
int g_count=0;//全局变量也可以在虚拟地址空间当中
void *rout(void *arg)
while(1)
{
printf("I am main %lx,%d\n",pthread_self(),g_count);
sleep(1);
}
int main()
{
while(1){
int *ptr;
++count;
printf("I am main %lx,%d\n",pthread_self(),g_count);
sleep(1);
}
}
return 0;
}
主线程循环一次,则count++;另外一个线程也可以感知到,说明共用同一块虚拟地址空间。
.2、局部变量(堆上)
(1) 代码
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<pthread.h>
# include<malloc.h>
void *rout(void *arg)
{
int *ptr=(int*) arg;//传参时,把void*转换为int*;所以在入口函数中需要把int*用同样的规则转换为void*
while(1){
printf("I am main %lx,%d\n",pthread_self(),*ptr);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,rout,(void*)ptr);
int *ptr=(int *)malloc((sizeof(int)));
*ptr=0;
while(1){
++*ptr;//此时并不是++ptr,而是指向的变量的值
printf("I am main %lx,%d\n",pthread_self(),*ptr);
sleep(1);
}
return 0;
}
3.、栈上
(1)代码
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<pthread.h>
# include<malloc.h>
int g_count=0;
void *rout(void *arg)
{
printf("I am main %lx,%d\n",pthread_self(),g_count);
sleep(1);
}
int main()
{
int count=0;
int *ptr=&count;
while(1){
int *ptr;
++count;
printf("I am main %lx,%d\n",pthread_self(),g_count);
sleep(1);
}
return 0;
}
打印的结果可以没有立刻被执行而是在缓冲区中呆了一会儿返回值可能会跑到前面;
一方面体现了缓冲区的存在;另一方面体现了线程的执行次序是不确定的。第二次打印时,主线程还没修改完毕,只修改了一半,这又说明和进程之间的调度有关系。同个函数共享同一个线程可能会出现问题,执行顺序,及执行到那儿的问题。为了保证得到的结果正确,需要给要访问的对象加入一些同步互斥机制,保证线程安全。
六.线程异常终止:任何一个线程出现问题都会导致整个进程出现错误。
1代码
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<pthread.h>
# include<malloc.h>
int g_count=0;
void *rout(void *arg)
{
printf("I am main %lx,%d\n",pthread_self(),g_count);
int*p=NULL;
p=100;//解引用空指针:进程会通过虚拟地址到物理地址的映射,映射过程中会发现这是一个空指针,是一个非法的地址。MM U会通知内核,内核会发送一个11号信号使进程异常终止。
sleep(1);
}
int main()
{
int count=0;
int *ptr=&count;
while(1){
int *ptr;
++count;
printf("I am main%lx,%d\n",pthread_self(),g_count);
sleep(1);
}
return 0;
}
结果会产生段错误,主线程也不会打印:说明任何一个线程出现问题都会导致整个进程出现错误。使代码对稳定性的要求更高。
七.如何利用多线程利用多核cpu资源:线程不是越多越好,应与cpu内核数匹配。
查看cpu信息:cat proc cpu
每个cpu都有一个编号,每个cpu有两个核:现在的电脑存在逻辑核数的概念和物理核数的概念。虽然只有一个cpu,但是可以模拟出两个逻辑核心。
1.某一个CPU满
1)代码
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<pthread.h>
# include<malloc.h>
void *rout(void *arg)
{
printf("I am main %lx\n",pthread_self());
sleep(1);
}
int main()
{
while(1){
;
}
return 0;
}
通过指令top可以查看thread进程,查看cpu占有率,发现只是吃满了cpu中 的一个(cpu%为100%)
2.2个cpu被占满
(1)代码
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<pthread.h>
# include<malloc.h>
void *rout(void *arg)
{
(void)arg;
while(1)
{
;
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,rout,NULL);
pthread_detach(tid);
while(1)
{
;
}
return 0;
}
此时查看cpu占有率发现为200%,说明吃满了2个cpu:虽然while没有做任何运算,只要代码里面做cpu计算都会一行一行的占用cpu代码。这些都属于cpu运行操作。Windows下不论有几个核,都不能存在200%,这种情况只在Linux下出现。【总共有8个核,极限条件下可能吃到800%】
3.4个进程
(1)代码
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<pthread.h>
# include<malloc.h>
void *rout(void *arg)
{
while(1){
;
}
}
int main()
{
const int size=4;
pthread_t tid[size];
int i=0;
for(;i<size;++i)
{
pthread_create(&tid[i],NULL,rout,NULL);
}
for(;i<size;++i){
pthread_join(tid[i],NULL);
}
return 0;
}
(2)结果
cpu吃到了400%
若是线程数超过总的cpu数,无论有多少线程,cpu个数是有限的,最多吃到cpu个数个就是其极限。说明线程不是越多越好,应该和cpu核数匹配。
.4.创建一个大的数组,完成操作:数组每个元素求平方值,结果放到对应的元素位上
//1 3 4 7
//1 9 16 49
(1) 使用单线程的情况://如果是单个线程,先遍历,再一个一个的去求
#include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<pthread.h>
# include<malloc.h>
获取时间戳函数:以1970年0时0分1秒为基准,计算到现在的时间
int64_t TimeStamp()//int64_t :64位的有符号整数
{
struct timeval tv;
gettimeofday(&tv,NULL);//第一个参数是一个结构体,包含当前函数秒级的时间戳和微秒级的时间戳
return tv.tv_sec*1000*1000+tv.tv_usec;//把秒数转化为微秒,得到当前时刻的时间
}
typedef struct Context{
int *ptr;
size_t beg;//要计算的数组的其实位置
size_t end;//要计算的数组的结束位置
}Context;//定义上下文信息
void *rout(void *arg)
{
Context *context=(Context *)arg;
size_t i=context->beg;
for(;i<context->end;++i){
context->ptr[i]=context->ptr[i]*context->ptr[i];
}
return NULL;
}
int main()
{
//一个比较大的数组,我们想完成这样的操作,把数组中的每一个元素
//都给你求一个平方的值,把这个结果放到对应的元素位置上
const size_t MAX_SIZE=10*1000*1000;
int *ptr=(int *)malloc(MAX_SIZE*sizeof(int));
const int size=1;
pthread_t tid[size];
int i=0;
int64_t beg=TimeStamp();
for(;i<size;++i){
Context *context=(Context *)malloc(sizeof(context));
context->ptr=ptr;
context->beg=0;
context->end=MAX_SIZE;
pthread_create(&tid[i],NULL,rout,(void *)context);
}
for(;i<size;++i){
pthread_join(tid[i],NULL);//返回则说明线程执行结束
}
int64_t end=TimeStamp();
printf(“time=%ld\n”,end-beg);
return 0;
}
(2) 2个线程,每个线程只占一半的区间,每个线程只计算其对应的去间
int64_t TimeStamp()
{
struct timeval tv;
gettimeofday(&tv,NULL);
return tv.tv_sec*1000*1000+tv.tv_usec;
}
typedef struct Context{
int *ptr;
size_t beg;//要计算的数组的其实位置
size_t end;//要计算的数组的结束位置
}Context;//定义上下文信息
void *rout(void *arg)
{
Context *context=(Context *)arg;
size_t i=context->beg;
for(;i<context->end;++i){
context->ptr[i]=context->ptr[i]*context->ptr[i];
}
}
int main()
{
const size_t MAX_SIZE=10*1000*1000;
int*ptr=(int *)malloc(MAX_SIZE*sizeof(int));
const int size=2;
pthread_t tid[size];
inti=0;
int64_t beg=TimeStamp();
size_t offset_beg=0;
size_t len=MAX_SIZE/size;//每个线程应该承载的数量
for(;i<size;++i){
Context *context=(Context *)malloc(sizeof(context));
context->ptr=ptr;
context->beg=offset_beg;
offset_beg+=len;
pthread_create(&tid[i],NULL,rout,(void *)context);
}
for(;i<size;++i){
pthread_join(tid[i],NULL);
}
int64_t end=TimeStamp();
return 0;
}
创建两个线程的执行时间比创建一个线程时间差不多少了一半,之所以不是严格意义上的一半是因为,线程创建和返回也是有时间开销的;线程也是会不停的调度的,其他线程也想调度干活,所以差不多为一半,而不是严格意义上的一半。
说明线程可以提升程序的运行效率。
继续加大线程,不会是几乎减小一半的时间,而是减小的时间很少,因为线程调度的开销也会有额外 的影响。