线程的创建和终止总结

符号 阅读:987 2021-05-14 10:25:33 评论:0

一、线程的概念
1、什么是线程?
线程指程序的一个执行流,也可理解为是进程内部的一个控制序列,任何进程都至少含有一个线程。如下图所示:
这里写图片描述

2、线程与进程的比较
进程是资源竞争的基本单位,线程是程序执行的最小单元。
(1)线程共享进程的资源
一个进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的。如果定义一个函数,在各线程中都可以调用;如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:
1)文件描述符表;
2)每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数);
3)当前工作目录
4)用户ID和组ID
(2)线程自己的数据
线程共享进程的数据,但也拥有自己的一部分数据:
1)线程ID;
2)一组寄存器;
3)栈;
4)errno;
5)信号屏蔽字:阻塞而不能传递的信号集;
6)调度优先级。
(3)创建线程的原理
进程是操作系统分配资源的最小单位,每当内核中新创建一个PCB的时候,就表示内核中新创建了一个进程。如下图,内核会为新创建的进程分配一系列的数据结构用来管理进程。
这里写图片描述

在linux中可以通过vfork()函数来创建一个子进程,在vfork分流之后、如果子进程中没有使用exec函数进行程序替换的话,则子进程会在父进程的地址空间中运行父进程的代码。其效果如图:
这里写图片描述
这时候PCB1和PCB就拥有相同的虚拟内存和相同的物理内存了,如果我们这时候再为PCB1创建一些他私有的资源(比如为他分配栈空间,给他独立出来一部分页表),那么PCB1就变成一个线程了。

(4)线程ID和进程地址空间布局
这里写图片描述
1)pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
2)前面说的线程ID属于进程调度的范畴,因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
3)pthread_create函数产生并标记在第一个参数指向的地址中的线程,属于NPTL线程库的范畴。线程库的后续操作就是根据该线程ID来操作线程的。
4)线程库NPTL提供了pthread_self函数,可以获得线程自身的ID。
函数原型为:pthread_t pthread_self(void);pthread_t 到底是什么类型,取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

3、线程的优点
(1)因为线程占用的资源要比进程占用的少,所以线程的创建和转换的代价要比创建一个新进程和进程间转换的代价小;
(2)I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作。
(3)在等待慢速I/O操作结束的同时,程序可执行其他的计算任务,能充分利用多处理器的可并行数量;
(4)计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。

4、线程的缺点
(1)性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与供它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
(2)健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
(3)缺乏访问控制:
进程是访问控制的基本力度,在一个线程中调用某些OS函数会对整个进程造成影响。
(4)编程难度提高:
编程与调试一个多进程程序比单线程程序困难多。

二、线程的创建
新增的线程可以通过调用pthread_create函数创建。
1、函数原型
int pthread_create(pthread_t thread, const pthread_attr_t *attr, void (start_routine)(void), void *arg);

2、参数
(1)thread:返回线程ID;
(2)attr:设置线程的属性,attr为NULL表示使用默认属性;
(3)start_routine:是个函数地址,线程启动后要执行的函数;
(4)arg:传给线程启动函数的参数。
3、返回值
成功返回0;失败返回错误码。
4、错误检查
(1)传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
(2)pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做),而是将错误代码通过返回值返回。
(3)pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码的函数。对于pthreads函数的错误,建议通过返回值以判定,因为读取返回值要比读取线程内的errno变量的开销更小。
【例】

#include<stdio.h> 
#include<stdlib.h> 
#include<unistd.h> 
#include<pthread.h> 
#include<string.h> 
#include<error.h> 
 
void *rout(void *argv) 
{ 
    for(; ;){ 
        printf("I am pthread!\n"); 
        sleep(2); 
    } 
} 
 
int main() 
{ 
    pthread_t tid; 
    int err; 
    err = pthread_create(&tid, NULL, rout, NULL); 
    if(err != 0){ 
        fprintf(stderr,"create pthread fail:%s!\n", strerror(err) ); 
        exit(EXIT_FAILURE);  
} 
 
    for(; ;){ 
        printf("I am main pthread!\n"); 
        sleep(2); 
    } 
    return 0; 
}

运行结果:
这里写图片描述
注意:
1)与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“thread_”开头的。
2)要使用这些函数库,要通过引入头文件“pthread.h”。
3)链接这些线程函数库时要使用编译器命令的“-lpthread”选项。

三、线程组
1、线程组的由来
(1)在Linux中,线程又被称为轻量级进程,每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
(2)没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系。POSIX标准要求进程内的所有线程调用getpid函数是返回相同的进程ID,如何解决上述问题?为此,Linux内核引入了线程组的概念。

2、线程组的概念
多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID。进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID。
(1)进程描述符如下:

struct task_struct{ 
    ... 
    pid_t pid; 
    pid_t tgid; 
    ... 
    struct task_struct *group_leader; 
    ... 
    struct list_head thread_group; 
    ... 
};

(2)进程和线程的区别如下图所示:
这里写图片描述
注:
1)getpid:取得process id,对于thread,就是取得线程对应进程的id;
2)gettid:取得线程id,如果是process,其实就等于getpid。
3)可以调用pthread_self库函数获取线程的id。

3、主线程
线程组内的第一个线程,在用户态被称为主线程(main thread),在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID设置成第一个线程的线程ID,group leader指针则指向自身,即主线程的描述符。所以线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。所下表示:
/线程组ID等于线程ID,group_leader指向自身/
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);
至于线程组中的其他线程的ID则由内核负责,其线程组ID总是和主线程的线程组ID一致,无论是主线程直接创建线程,还是创建出来的线程再次创建线程,都是这样。如下表示:

if(clone_flags & CLONE_THREAD) 
    p->tgid = current->tgid; 
if(clone_flags & CLONE_THREAD){ 
    p->group_lead = current->group_leader; 
    list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group); 
} 

强调一点:线程和进程不一样,进程有父进程的概念,但是在线程里面,所有的线程都是对等关系。如下图所示:
这里写图片描述

【例】获取进程和线程ID

#include<stdio.h> 
#include<stdlib.h> 
#include<unistd.h> 
#include<pthread.h> 
 
pthread_t ntid; 
 
void printftid(char *s) 
{ 
    pid_t pid; 
    pthread_t tid; 
 
    pid = getpid(); 
    tid = pthread_self(); 
    printf("%s pid %lu tid %lu", s, (signed long)pid, (signed long)tid); 
} 
 
void *newpthread(void *arg) 
{ 
    printftid("new pthread:"); 
    printf("\n"); 
    return ((void*)0); 
} 
 
int main() 
{ 
 
    int ret = pthread_create(&ntid, NULL, newpthread, NULL); 
    if(ret != 0){ 
        perror("pthread_create!"); 
    } 
    printftid("main pthread:"); 
    printf("\n"); 
    sleep(1); 
    exit(0); 
}

运行结果:
这里写图片描述
释:主线程和新线程的pid相同,但是tid不同,表示它们是在同一进程里运行的两个线程。其实,线程的ID是在虚拟地址空间里的一个地址,即指针,用无符号长整型表示。

4、查看线程组
现在介绍的线程ID,不同于pthread_t类型的线程ID,和进程ID一样,线程ID是pid_t类型的变量,而且是用来唯一标识线程的一个整型变量。如何查看一个线程的ID呢?
【例】ps命令中的-L选项,会显示如下信息:
这里写图片描述
释:
LWP:线程ID,既gettid()系统调用的返回值。
NLWP:线程组内线程的个数。
从上图可看出,在单线程进程中,线程和进程的ID相同。

注:
Linux提供了gettid系统调用来返回其线程ID,可是glibc并没有将该系统调用封装起来,再开放接口来供程序员使用。如果确实需要获得线程ID,可以采用如下方法:

#include<sys/syscall.h> 
pid_t tid; 
tid = syscall(SYS_gettid);

四、线程的终止
1、终止的方法
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
(1)从线程函数return,但是这种方法对主线程不适用,从main函数return相当于调用exit。
(2)线程可以调用pthread_exit终止自己。
(3)一个线程可以调用pthread_cancel终止同一个进程中的另一个线程。

2、终止的函数
(1)pthread_exit函数
1)功能:线程终止
2)函数原型:void pthread_exit(void* value_ptr)
3)参数:value_ptr不要指向一个栈内存(参数或局部变量)。
4)返回值:无返回值,跟进程一样,线程结束的时候返回到它的调用者(自身)。
5)注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局变量或者是malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。

(2)pthread_cancel函数
1)功能:取消一个执行中的线程。
2)函数原型:int pthread_cancel(pthread_t tid);
3)参数:tid表线程ID
4)返回值:成功返回0,失败返回错误码。


声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

我的关注

全民解析

搜索
排行榜
关注我们