线程的概念+线程函数API
创始人
2024-03-13 00:56:46
0

C线程

有关线程的简单实现

#include
#include
#include
//定义线程函数(固定)--void *
void *pth_fun(void *pth_arg){while(1){printf("pthread\n");sleep(1);}return NULL;
}
int main(){pth_fun(NULL);//先将线程函数作为普通函数调用return 0;
}
guojiawei@ubantu-gjw:~/Desktop/pthread$ gcc pthread.c 
guojiawei@ubantu-gjw:~/Desktop/pthread$ ./a.out 
pthread
pthread
pthread
pthread
pthread
^Xpthread
pthread
^Z
[1]+  已停止               ./a.out

#include
#include
#include
#include 
//定义线程函数(固定)--void *
void *pth_fun(void *pth_arg){while(1){printf("child_pthread\n");sleep(1);}return NULL;
}
int main(){/*将函数注册为线程函数*///int pthread_create(pthread_t *thread, const pthread_attr_t *attr,\void *(*start_routine) (void *), void *arg);pthread_t tId=0;//函数名就是地址pthread_create(&tId,NULL,pth_fun,NULL);//主线程while(1){printf("father\n");sleep(2);}return 0;
}

程序结果:

可以自己链接线程库-pthread,但是现在可以不加

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Kq6cFYd-1669961351681)(/home/guojiawei/.config/Typora/typora-user-images/image-20221129145844582.png)]

主线程和次线程是并发运行的,主线程不运行,次线程无法运行

  • 其实主线程是一个进程,目的为了带动线程运行
  • 线程就是一个函数,有的语言会封装

学习C线程意义

  • c程序经常涉及到多线任务的问题,所以c线程在实际开发中会被经常用到

    使用多线程的话,必须要OS支持

  • 所有的线程库的原理和使用方式都是类似

    理解了C线程函数,在学习其它语言的线程库函数就会容易很多

为什么会有线程

为了弥补进程的缺点

进程最大的优点:拥有独立的进程空间

可以很好地保证每一个进程的安全,不被其它进程所攻击或者干扰,

进程的缺点:(也是优点带来的)

  • 进程间切换的计算机资源开销很大,切换效率非常低
  • 进程间数据共享的开销也很大

为什么进程之间切换资源开销大

​ OS是通过虚拟内存机制来实现进程空间独立的

  • 进程在并发运行时需要相互间的切换,切换时必然需要涉及虚拟内存机制的控制

  • 但是虚拟内存机制比较复杂,所以在进行进程间切换时,会耗费高昂的cpu、缓存(cache)、内存等计算机资源,也非常耗费切换时间

进程通信的开销大

​ 当程序涉及多进程时,往往会涉及到进程间的通信,由于进程空间的独立性,OS提供了各种各样的通信机制:

这些通信机制共同原理:通过OS来转发进程间的数据

但是调用OS提供的这些通信机制的函数时,这些OS函数的运行也是需要消耗相当cpu、内存等计算机资源的,同时也很耗费时间


对于有OS的计算机来说:

虽然进程是必不可少的,但是进程确又不能太多,进程太多会导致计算机资源被剧烈消耗,此时你会发现你的计算机非常的卡

创建多进程目的

目的1:创建子进程,执行新程序

目的2:创建子进程得到多进程,通过多进程并发实现多线任务

多线任务例子

  1. 同时阻塞的读鼠标和键盘时,如果单线的话会想互影响,需要两线任务来实现

    比如:父进程读鼠标,子进程读键盘

  2. 读写管道时,读操作是阻塞的,为了避免读操作影响写操作,也需要两线任务同时进行

    父子进程一个读管道,一个写管道

  • 第一种目的,执行新程序时必须创建子进程,这个无法逃避的

  • 第二种目的,使用多进程来实现就存在巨大的问题

    因为几乎所有的程序都涉及多线任务的操作

多线任务使用多进程的问题

程序往往都是十几个任务以上,如果此时使用多进程来实现多线任务的,这就大致大量进程的产生

比如计算机运行了100个程序,假设每个程序平均10多个任务,如果全部采用多进程来实现,计算机最终要运行的进程就多达上100个

总结:

进程切换和进程间通信的计算机资源开销又很大,往往导致计算机非常卡顿,程序的运行效率非常低

线程的发明目的:弥补多进程实现多线任务的缺点。

线程为什么可以弥补进程的缺点

线程与进程一样,线程和进程会被OS统一调度,所以线程和进程都是一起并发运行的

实现程序的多线任务的前提就是:并发执行

有了线程以后,凡是程序涉及到多线任务时,都使用多线程来实现

  • 线程间的切换和数据通信的开销非常低
  • 线程还有另一个名称,叫“轻量级的进程”-----开销非常低

说白了线程就是为了多线任务而生的

为什么线程切换的开销低

使用多线程来实现多线任务时:

线程本质上它只是程序(进程)的一个函数,与普通函数的区别是:普通函数时单线的运行关系,而线程函数被注册为线程后,是多线并发运行。

简单理解就是:
1.普通函数的执行是:堆栈,也就是遇到子函数去执行子函数,再遇到子函数,切换到调用的子函数,执行完就返回到调用函数,直至结束,所以是单线
2.而线程是并发的,不存在相互调用,只能时间片切换,所以是多线
--
线程函数也可以去调用普通函数
  • 对于普通函数来说,只有当相互调动时才会涉及函数间的切换
  • 对于线程函数来说,只要运行的时间片到了就会切换

不管是函数间的切换,还是线程的切换都是进程内部的事情

不涉及进程间的切换,故而省去了进程间切换的巨大开销

但是两个线程分别存在于不同的进程,此时两个线程切换依然需要进程切换

  • 线程的切换其实就是函数间的切换,函数切换当然也需要开销,但是这些开销非常小

为什么线程间数据共享的开销很低

线程的本质就是函数,所以线程通信属于函数通信

函数间通信有两种方式:(1)具有相互调用关系函数来说使用函数传参来通信。(2)对于没有调用关系的函数来说使用全局变量来通信。A函数 ————> 全局变量 ————> B函数
全局变量的作用是什么?

用来实现无调用关系的函数间通信


进程中所有的线程函数除了相互并发运行外,没有调用关系

所以线程函数间想要数据共享的话,就使用全局变量来通信


线程为什么不能替代进程

线程的本质是函数,函数运行需要内存空间,线程运行的内存空间就是进程的内存空间

线程运行时必须依赖于进程的存在,如果没有进程所提供的内存空间这个资源,线程根本无法运行

线程作为函数,只是进程的一个部分而已,线程是不可能脱离进程而独立存在

  • 同一个进程中的所有线程,都是运行在相同的进程空间中的
  • 换句话说同一个进程中所有线程共相同的进程空间

既然进程空间是共享的,那么所有的函数,自然就能共享访问在共享空间中所开辟出来的全局变量

线程独立的属性

线程共享进程空间的资源
  • 全局变量
  • 工作目录(进程的当前工作目录)
  • 打开的文件描述符
  • 子函数
  • 进程uid、gid,进程PID等等
线程的独立属性

并发运行的就是一个单独执行体

  • 每个线程拥有自己独立的线程ID(TID)

  • 每个线程有独立的切换状态

    • 在切换时,当前线程被中断的那条指令的地址
    • 线程切换时相关的运行状态

    说白了就是:保存现场,便于还原

  • 有自己独立的函数栈

    每一个函数都有自己的函数栈,所有的函数栈都开辟于进程空间的进程栈

    函数栈的作用就是用来保存函数局部变量

  • 自己独立的错误号

    线程函数出错时,错误号并不是通过设置errno实现的,而是直接将错误号返回

  • 每一个线程有自己独立的信号屏蔽字未决信号集

  • 每个线程有自己独立的tack_struct结构体

    • 管理线程时:也会为线程开辟一个task_struct变量
    • 只不过适用于存放的是线程的管理信息

创建子进程执行新程序,大多都是OS操心的事

比如:父进程(命令行、图形界面)创建子进程并加载执行新程序

但是我们自己的程序很少使用多进程,一般使用多线程

线程控制函数

  • pthread_create()
  • pthread_join()
  • pthread_detach()
  • pthread_cancel()
  • pthread_exit等

线程函数由谁提供

进程控制的fork、exec等函数都是由os系统提供的

为了不给OS增加负担,同时也为了提高线程的灵活性:

后来的线程不由OS提供,而是由单独的线程库来提供

不过线程库在实现时,也是调用了相应的系统API的

线程库

c线程库并不是C标准库,而是POSIX C库的一部分

java、c++、c#的线程函数由他们自己的线程库来实现的

  • java、c++的线程函数会比c的线程复杂一些

线程库和OS系统API的关系

线程库函数实际上也是封住OS的相应API来实现的

线程库运行在Linux,通过调用Linux的clone()等系统函数实现

将线程函数注册为线程时:
其实就是通过调用这类系统API,然后去模拟我们的进程来实现的
正是因为是:模拟进程来实现的,所以线程函数才能进程一样,一起被并发运行

可以自己调用系统API,然后封装做出自己的c/c++/java线程库

----不过没啥价值和意义,因为有现成的

C线程控制函数

pthread_create()

#include 
//把第三个参数的函数注册成为一个线程函数
//该函数一旦注册成功,这个函数就以次线程的方式开始并发运行int pthread_create(pthread_t *thread, const pthread_attr_t *attr,\void *(*start_routine) (void *), void *arg);
/*返回值成功返回0,失败返回非零错误号*/

Compile and link with -pthread

目的主要是实现并发,而且该注册函数是次进程

  • 凡是使用pthread_create注册的线程都是次线程,次线程会和主线程一起并发运行。

void* (*start_routine) (void *):参数和返回值都是void *,函数名就是地址

参数
  1. thread------注意传指针,需要取地址

  2. attr----(attibute属性)

    是个重命名的结构体,用于设置线程属性

    • 设置线程属性是为了实现某些特殊功能
    • 设置为NULL,表示不设置特有的属性,使用线程默认属性所提供的功能

    正常情况下,线程默认属性所提供的功能就已经够用了

  3. 要注册为线程的函数地址

如果不注册,线程函数就是一个普通的函数

//线程函数需要我们自己定义
void *pth_fun(void *pth_arg){...//线程要做的事情}
//pth_fun和pth_arg的命名由自己决定。
  1. arg—传递给线程函数的参数

传递给线程函数的参数,这个参数会传递给pth_arg

  • 如果参数很多的话,定义一个结构体,然后把结构体变量的地址传过去
  • 如果不传递参数,设置为NULL

总结:pthread(进程号地址,设置属性(一般NULL),函数名地址,线程函数的传参)

程序----两个次进程向文件写数据

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define SECOND_PTH_NUMS 2
void pth_print_err(char * str,int err){printf("%s:%s",str,strerror(err));exit(-1);
}
//次线程结构体参数
typedef struct pthread_arg{pthread_t tid;int pth_no;//线程编号int fd;//打开的文件描述符
}ptharg;
//线程普通函数
void *pth_fun(void *pth_arg){int fd=((ptharg *)pth_arg)->fd;while(1){printf("%d",((ptharg *)pth_arg)->pth_no);write(fd,"hello ",6);write(fd,"world\n",6);sleep(1);}return NULL;
}
int main(){int i,ret;int fd=open("./mmm.txt",O_CREAT|O_TRUNC|O_RDWR,0664);if(fd==-1) pth_print_err("open fails\n",errno);/*将函数注册为线程函数,传建两个次线程int pthread_create(线程号地址,设置属性,函数地址,线程函数传参);*/ptharg pth_arg[SECOND_PTH_NUMS];for(int i=0;ipth_arg[i].fd=fd;pth_arg[i].pth_no=i;ret= pthread_create(&pth_arg[i].tid,NULL,\pth_fun,(void *)&pth_arg[i]);if(ret!=0){pth_print_err("pthread_create fails\n",ret);}}while(1){printf("father\n");sleep(2);}return 0;
}

在这里插入图片描述

需要强调的地方

  • gcc编译时,跟-pthread选项(现在可以不加)

  • 父子线程调用子函数不冲突

    • main函数调用子函数时,子函数属于主线程这条线
    • 次线程调用子函数时,子函数属于次线程这条线。
  • 主线程代表了整个进程的存在

    • 主线程结束了,整个进程也就结束了,进程都没了线程自然也没了
    • 所以主线程一定不能死
    • 次线程结束了,对整个进程没有任何影响
  • C线程函数的启动与c++/java线程函数启动的略微有所不同

    • c++/java的线程函数被注册为线程后不会立即启动,需要单独调用某个启动函数来启动

    c线程函数一旦被pthread_create注册为线程后会立即被启动运行

    • c++、java等面向对象的函数,都被封装在类里面,包括线程函数也是如此

      而c这种面向过程语言的函数,全部都裸露在外的

pthread_cancel()

#include 
//当次线程是死循环时,可以调动这个函数主动取消该线程
//返回值:成功返回0,失败返回非零错误号
int pthread_cancel(pthread_t thread);
//参数------thread:要取消线程的TID
//Compile and link with -pthread

//设置全局变量,供signal_fun使用
typedef struct thread_args{pthread_t thrId;int thrNo;int fd;
}threadArg;
threadArg thr[2];
//取消两个次线程(信号捕捉)
void signal_fun(int signo){if(SIGALRM==signo){int i=0;for(;i

使用编号来结束

好处是,完整执行程序后,再去判断是否该杀死

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define PTH_NUMS 2
#define PTHEXIT  -1
typedef struct thread_args{pthread_t thrId;int thrNo;int fd;
}threadArg;
//结构体封装所有全局变量
struct global_vals{threadArg thr[2];int pth_exit_flag[PTH_NUMS];
}globalVal;//全局变量初始化为0
void print_err(int err,char* str){printf("%s:%s",str,strerror(err));exit(-1);
}
void *pthread_fun(void *arg){int fd=((threadArg *)arg)->fd;int thrNo=((threadArg*)arg)->thrNo;pthread_t thrId=((threadArg *)arg)->thrId;/*线程功能*/while(1){printf("线程号:%lu,线程编号:%d\n",thrId,thrNo);write(fd,"hello,",6);write(fd,"world\n",6);if(globalVal.pth_exit_flag[thrNo]) break;sleep(1);}return NULL;
}
void signal_fun(int signo){if(SIGALRM==signo){int i=0;for(;iglobalVal.pth_exit_flag[i]=PTHEXIT;pthread_cancel(globalVal.thr[i].thrId);}}
}
int main(void){int fd=open("./threadFile",O_CREAT|O_TRUNC|O_RDWR,0664);int i,ret;for(i=0;iglobalVal.thr[i].fd=fd;globalVal.thr[i].thrNo=i;ret=pthread_create(&globalVal.thr[i].thrId,NULL,\pthread_fun,(void *)&globalVal.thr[i]);if(ret!=0)print_err(ret,"pthread_create fails\n");}//定时10s,时间到后取消次线程signal(SIGALRM,signal_fun);alarm(10);//父线程while(1){printf("father\n");sleep(2);}return 0;
}

pthread_exit()

#include 
//线程调用这个函数时,可以主动退出(终止)
//返回值:成功返回0,失败返回非零错误号
//参数----------retval:线程结束时的返回值。
void pthread_exit(void *retval);
/*如果返回值很多时,就封装成一个结构体,返回结构体变量的地址即可*/
  • 类似于exit函数,不过exit是终止整个进程的
  • pthread_exit是终止次线程

事实上:线程也可以通过调动return来退出(终止)

pthread_self()

#include 
//功能:线程获取自己的TID,类似于进程调用getpid()获取自己的PID一样
pthread_t pthread_self(void);
//返回值:成功返回线程TID,失败返回非零错误号。

使用:

 printf("fatherTid:%lu\n",pthread_self());//主线程TID

pthread_join()

#include 
/*功能:
阻塞等待tid为thread的次线程结束,结束时该函数会回收次线程所占用的所有资源(存储空间)
*/
//返回值:成功返回0,失败返回错误号
int pthread_join(pthread_t thread, void **retval);

这个函数只对次线程有意义,对主线程没有意义

  • 因为主线程结束时整个进程就结束了,整个进程资源会由父进程回收
  • 这个函数一般都是由主线程调用,以回收相关次线程的资源
  • 当然次线程也是可以调用这个函数来回收其它次线程资源的
回收次线程资源原因
  • 有些程序(进程)一旦运行后将会长期运行,不会结束
  • 如果不回收,每结束一个次线程就导致一部分资源被占用
  • 慢慢累积会使得整个进程资源越用越少,最好导致进程崩溃

说白了就是:次进程不断占据运行的进程的资源,最后过多而使得进程崩溃

参数:
(a)thread:指定要回收次线程的TID
(b)retval:次线程函数返回的返回值

``参数二取的是void * 返回值的地址,所以是两个*`

使用
 //次进程函数里面的返回值
//return NULL;pthread_exit((void *)10);
//循环接收次进程资源(阻塞)void *retval=NULL;for(i=0;ipthread_join(globalVal.thr[i].thrId,&retval);printf("%p\n",retval);}

​ 如果线程是被pthread_cancel取消掉的,自动返回-1

pthread_detach()

#include 
/*功能:如果次线程的资源不希望别人调用pthread_join函数来回收的话,而是希望自己在结束时,自动回收资源的话,就可以调用这个函数。
*/
//返回值:成功返回0,失败返回错误号
//参数:thread:你要分离的那个次线程的TID。
int pthread_detach(pthread_t thread);

这个函数的功能就是分离次线程,让次线程在结束时自动回收资源

使用:

 //次进程资源自动回收pthread_detach(globalVal.thr[i].thrId);
  • pthread_joinpthread_detach作为两种不同的线程资源回收方式,只能二选一
注释代码的方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4gv5A57j-1669961351684)(/home/guojiawei/.config/Typora/typora-user-images/image-20221130184244462.png)]

注册进程退出处理函数

  • 进程程在退出时会自动调用atexit();,实现进程的扫尾处理
回顾进程处理函数:atexit
void process_deal(){printf("\ndealing process!!\n");
}
//函数无返回值无传参
/*进程处理函数*/atexit(process_deal);

把ctrl+c变成正常退出

void signal_fun(int signo){if(SIGALRM==signo){int i=0;for(;iglobalVal.pth_exit_flag[i]=PTHEXIT;pthread_cancel(globalVal.thr[i].thrId);}}else if(signo==SIGINT){exit(0);}
}

signal(SIGINT,signal_fun);

  • 线程也是这样
 #include 
//注册函数
void pthread_cleanup_push(void (*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);
  1. pthread_cleanup_push
  • 将类型为void (*routine)(void *)函数注册为“线程退出处理函数”

    注意第一个没*,第二个有*

  • arg:传递给退出处理函数的参数

  1. pthread_cleanup_pop

执行这个函数时,参数:

  • 如果参数写!0

    会将压入栈中的推出处理函数地址弹出,然后调用退出函数进行线程的扫尾处理。

  • 如果参数写0

    不弹出调用

    注册的原理就是将处理函数地址压入线程栈
----可以反复调用该函数注册多个退出处理函数,但是一般一个就够了----注册了多个线程退出处理函数的话,由于栈先进后出的特点注册压栈的顺序与弹栈调动的顺序刚好相反

有一个pthread_cleanup_push,就必须要对应有一个pthread_cleanup_pop

就算这个函数调用不到也必须写,否则编译时不通过

就像{}是成对出现的,缺一个都会报错

例子
/*线程退出处理函数*/
void pth_exit_deal(void* t_arg){printf("pthread %lu exit\n",((threadArg*)t_arg)->thrId);
}
/*进程函数*/
void *pthread_fun(void *arg){//注册 线程退出处理 函数pthread_cleanup_push(pth_exit_deal,arg);pthread_cleanup_pop(!0);pthread_exit((void *)10);
}

弹栈线程退出处理函数的几种条件

(a)调用thread_cleanup_pop(!0),主动弹栈

(b)如果线程是被别人调用pthread_cancel取消的,也会弹栈

(c)如果线程是调用pthread_exit函数退出的,也会弹栈

如果线程时调用return退出的话,是不会自动弹栈的
要弹栈的话,必须主动调动thread_cleanup_pop(!0)			

线程的属性设置

就是pthreadcreate()的第二个参数不要写NULL

可以设置的线程属性
  • 设置绑定属性
  • 设置分离属性
  • 设置线程堆栈属性
  • 设置线程调度优先级属性

事实上默认属性所提供的功能就已经足够我们使用了,

C线程中设置属性的函数非常多,基本每一种属性都有独立的设置函数
例子:设置分离属性

将线程分离有两种方法:

  • 调用pthread_detach函数实现

  • 通过设置分离属性实现

    事实上使用pthread_detach()更方便些

分离属性设置的步骤:

  1. 定义一个变量来存放新属性

    pthread_attr_t attr;//一个结构体,被typedef重命名了
    
  2. 调用pthread_attr_init(&attr);初始化一下attr结构体变量

    int pthread_attr_init(pthread_attr_t *attr);
    //成功返回0,失败返回错误号
    
  3. 调用pthread_attr_setdetachstate预设分离属性

    想设置什么类型属性就选择什么函数

    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
    
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    

    指定PTHREAD_CREATE_DETACHED宏后:

    pthread_attr_setdetachstate函数会自动的将:

    分离属性相关的数据设置到attr结构体变量的各个成员中

  4. 调动pthread_create创建线程时,将attr传递给pthread_create,将线程分离

  5. 删除属性设置

    int pthread_attr_destroy(pthread_attr_t *attr);
    //成功返回0,失败返回错误号
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define PTH_NUMS 2
    #define PTHEXIT  -1
    typedef struct thread_args{pthread_t thrId;int thrNo;int fd;
    }threadArg;
    //结构体封装所有全局变量
    struct global_vals{threadArg thr[2];int pth_exit_flag[PTH_NUMS];pthread_attr_t attr;//设置线程属性
    }globalVal;//全局变量初始化为0
    void print_err(int err,char* str){printf("%s:%s",str,strerror(err));exit(-1);
    }
    /*线程退出处理函数*/
    void pth_exit_deal(void* t_arg){printf("pthread %lu exit\n",((threadArg*)t_arg)->thrId);
    }
    void *pthread_fun(void *arg){int fd=((threadArg *)arg)->fd;int thrNo=((threadArg*)arg)->thrNo;pthread_t thrId=((threadArg *)arg)->thrId;//pthread_detach(pthread_self());//注册 线程退出处理 函数pthread_cleanup_push(pth_exit_deal,arg);/*线程功能*/while(1){printf("线程号:%lu,线程编号:%d\n",thrId,thrNo);write(fd,"hello,",6);write(fd,"world\n",6);if(globalVal.pth_exit_flag[thrNo]) break;sleep(1);}//pthread_exit((void *)10);return NULL;pthread_cleanup_pop(!0);
    }
    void signal_fun(int signo){if(SIGALRM==signo){int i=0;for(;iglobalVal.pth_exit_flag[i]=PTHEXIT;//退出标志//pthread_cancel(globalVal.thr[i].thrId);//主动}}else if(signo==SIGINT){pthread_attr_destroy(&globalVal.attr);exit(0);}
    }
    void process_deal(){printf("\ndealing process!!\n");
    }
    int main(void){/*进程处理函数*/atexit(process_deal);int fd=open("./threadFile",O_CREAT|O_TRUNC|O_RDWR,0664);int i,ret;//属性初始化ret=pthread_attr_init(&globalVal.attr);if(ret!=0) print_err(ret,"attr fails\n");//设置分离属性pthread_attr_setdetachstate(&globalVal.attr,PTHREAD_CREATE_DETACHED);//线程初始化for(i=0;iglobalVal.thr[i].fd=fd;globalVal.thr[i].thrNo=i;ret=pthread_create(&globalVal.thr[i].thrId,\&globalVal.attr,\pthread_fun,(void *)&globalVal.thr[i]);//次进程资源自动回收pthread_detach(globalVal.thr[i].thrId);if(ret!=0)print_err(ret,"pthread_create fails\n");}//信号异常终止变成正常终止signal(SIGINT,signal_fun);//定时10s,时间到后取消次线程signal(SIGALRM,signal_fun);alarm(3);#if 0//循环接收次进程资源(阻塞)void *retval=NULL;for(i=0;ipthread_join(globalVal.thr[i].thrId,&retval);printf("%ld\n",(long)retval);}#endif//父线程while(1){printf("fatherTid:%lu\n",pthread_self());//主线程TIDsleep(2);}return 0;
    }
    

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...