[LINUX-08]浅析多线程与多线程相关的简单函数
【概念】
在这里我们只做简单理解,因为说得越多反而越不利于理解,具体理解参考点击打开链接。
我们就把线程看成是在一个程序里的一个执行路线,更准确的说线程是一个进程内部的执行流。
【进程与线程的区别】
为了更直观的区别他们两,我们用下面的图来区别~
他们的区别总结如下:
(1)进程是承担系统资源分配的基本单位,线程是程序调度的基本单位
(2)线程共享进程资源(文件描述符表、信号处理方式、当前工作目录、用户ID和组ID等),单页拥有自己的一部分数据,如线程ID(TID)、上下文信息、寄存器信息、栈指针栈空间、errno变量、信号屏蔽字、调度优先级、线程私有数据等等。
线程的优点:注:相比进程而言
(1)创建代价小--->创建一个新线程的代价远比创建一个新进程的代价要小得多
(2)切换不费力--->线程间切换需要操作系统做的工作比进程间切换要少得多
(3)占用资源少--->线程占用的资源比进程少得多
(4)充分利用资源--->能够充分利用多处理器的可并行数量
(5)在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
(6)计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
(7)I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作
线程的缺点:
(1)性能损失
一个很少被外界时间阻塞的计算密集型线程旺旺无法与其它线程共享同一个处理器,如果计算密集型线程的数量比处理器多,那么可能会有较大的性能损失,这里的性能损失是指增加了额外的同步和调度开销,可用的资源不变
(2)健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量二造成不良影响的可能性很大,换句话说线程之间是缺乏保护的
(3)缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个线程早成影响
(4)编程难度提高
编写与调试一个多线程程序比单线程程序困难的多
线程控制函数
【注】与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以pthread_d打头的,要使用这些函数库,要通过引入头文件pthread.h,链接这些线程函数库时要使用编译器命令的-lpthread选项
同进程ID一样,线程ID是pid_t类型的变量,而且是用来唯一标识线程的一个整型变量,在linux当中如果你要查询系统中的线程,也有一条命令:ps -aL
在传统的UNIX进程模型中,每个进程只有一个控制线程,从概念上讲,这与基于线程的模型中每个进程只包含一个线程是相同的;在POSIX线程的情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的,在创建多个控制线程以前,程序的行为与传统的进程没有什么区别。
也就是说没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID,但是引入线程后,一个用户进程下管辖N个用户级线程,每个线程作为一个独立的调度实体再内核态都有自己的进程描述符,进程和内核的年描述符一下子就编程了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何让解决上述问题呢?
Linux内核引入了线程组的概念
struct task_struct { ... pid_t pid;//线程id pid_t tgid;//进程id ... struct task_struct *group_leader;//主线程 ... struct list_head thread_group;//线程组 ... };
【注】
(1)多线程的进程,又被称为线程组,线程组内的每⼀个线程在内核之中都存在⼀个进程描述符(task_struct)与之对应。
(2)进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID
(3)进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID
那么如何让查看进程ID呢?
(1)线程组:线程内的第一个线程,再用户态被称为主线程,在内核中被称为group_leader
(2)group_leader指针指向自身,即主线程的进程描述符
/* 线程组ID等于线程ID,group_leader指向⾃自⾝身 */ p->tgid = p->pid; p->group_leader = p; INIT_LIST_HEAD(&p->thread_group);
(3)线程组中其他线程的ID由内核负责分配,无论是主线程直接创建线程还是创建出来的线程再次创建线程,线程组ID总是和主线程的线程组ID一样
(4)线程组里面,所有线程都是对等关系
Linux提供了gettid系统调用来返回线程ID,可是glibc并没有将该系统调用封装起来,在开放接口来供我们使用,如果确实要获得线程ID,可以采取如下方法
#include <sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);
新增的线程可以通过调用pthread_create函数创建
pthread_create---->创建新线程
函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
函数参数
thread:返回线程ID//这里用的是pthread_self获得tid号
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值
成功返回0,失败返回错误码
代码实例
#include<stdlib.h> #include<unistd.h> #include<string.h> #include <stdio.h> #include <pthread.h> void *run(void *arg) { int ret=(int)arg; while(1){ printf("new thread running------;tid = %lu,arg = %d\n",pthread_self(),ret); sleep(1); } } int main( void ) { pthread_t tid; pthread_t tid1; pthread_t tid2; int ret = pthread_create(&tid, NULL, run, (void*)0); if (ret != 0 ) { fprintf(stderr, "pthread_create : %s\n", strerror(ret)); exit(EXIT_FAILURE); } int ret1 = pthread_create(&tid1, NULL, run, (void*)1); if (ret1 != 0 ) { fprintf(stderr, "pthread_create : %s\n", strerror(ret1)); exit(EXIT_FAILURE); } int ret2 = pthread_create(&tid2, NULL, run, (void*)2); if (ret2 != 0 ) { fprintf(stderr, "pthread_create : %s\n", strerror(ret2)); exit(EXIT_FAILURE); } while(1){//main thread printf("I am main thread------;tid = %lu\n",pthread_self()); sleep(1); } return 0; }
运行结果
获取线程ID的方法:
(1)直接调用pthread_self()------>获取的是POSIX描述的线程ID
函数原型:
pthread_t pthread_self(void);
(2)gettid或者类似gettid的方法----->获取的是内核中线程ID
pid_t gettid(void)
(3)对于单线程的进程,内核中tid=pid;对于多线程进程,pid相同tid不同;tid用于描述内核真实的pid和tid信息
(4)pthread_self 返回的是POSIX定义的线程ID,他只是用来区分某个进程中不同的线程,当一个线程退出后,新创建的线程可以复用原来的id;gettid获取的是内核中真实线程ID
(5)具体区别请戳点击打开链接
线程终止
线程终止的三种方法(这里指不终止进程)
(1)从线程函数return,这种方法对主线程不适用,从main函数return相当于调用exit,进程直接终止掉了
#include<unistd.h> #include<string.h> #include <stdio.h> #include <pthread.h> 6 void *run(void *arg) { 7 int ret=(int)arg; 8 int count=5; 9 while(count--){ 10 printf("new thread running------;tid = %lu,arg = %d\n",pthread_self(),ret); 11 sleep(1); 12 } 13 return (void*) ret; 14 } 15 int main( void ) 16 { 17 pthread_t tid; 18 pthread_t tid1; 19 pthread_t tid2; 20 pthread_create(&tid, NULL, run, (void*)0); 21 pthread_create(&tid1, NULL, run, (void*)1); 22 pthread_create(&tid2, NULL, run, (void*)2); 23 //main thread 24 int s,s1,s2 = 0; 25 pthread_join(tid,(void**)&s); 26 pthread_join(tid1,(void**)&s1); 27 pthread_join(tid2,(void**)&s2); 28 printf("I am main thread------;tid = %lu\n",pthread_self()); 29 printf("exit code :%d,%d,%d\n",s,s1,s2); 30 return 0; 31 }
运行结果:
(2)线程可以调用pthread_exit终止自己
将第一个代码修改为:
return (void*) ret;----->修改为pthread_exit((void*)(10-ret));
运行结果
(3)一个线程可以调用pthread_cancel终止同一个进程中的另一个线程
将第一个代码修改如下:
运行结果如下:
pthread_exit函数
原型:
void pthread_exit(void *value_ptr)
参数:
value_ptr:value_ptr不要指向一个局部变量
返回值:
无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
【注】
pthread_exit或return 返回的指针所指向的内存单元必须是全局的或者malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel函数
功能:取消一个执行中的线程
原型:
int pthread_cancel(pthread_t thread)
参数:thread--->线程id
返回值:成功返回0,失败返回错误码
pthread_join函数--->线程等待函数
函数原型:
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
value_ptr:指向一个指针,后者指向线程的返回值
返回值:
成功返回0,失败返回错误码
【实例】
运行结果
调用该函数的线程将挂起等待,直到id为thread的线程终止thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
1. 如果thread线程通过return返回,value_ ptr所指向的单元⾥里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元⾥里存放的是常数 PTHREAD_ CANCELED。
3. 如果thread线程是自己调用pthread_exit 终止的 ,valueptr所指向的单元存放的是传给pthread_exit的参数。
4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。(这点上面有用到过,就不使用了)
如何查看线程?
我们简化上面的代码
然后我们用命令查看一个我们刚刚创建的线程
pthread_detach----->分离线程
函数原型
pthread_detach(pthread_t thread);
功能:也可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
pthread_detach(pthread_self);
【注】默认情况下,新创建的线程是等待的,线程推出后,需要对其pthread_join操作的,否则无法释放资源,从而造成系统泄露,如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
joinable和分离是冲突的,一个线程不能同时进行joinable和分离,下面验证该观点
运行结果分析
原文链接:https://blog.csdn.net/dove1202ly/article/details/80265764