Linux多线程1

一、线程的概念:
1、什么是线程
(1)在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
(2)一切进程都有一个执行线程。

2、进程和线程
(1)进程是资源竞争的基本单位。
(2)线程是程序执行的最小单位。
(3)线程共享进程的数据,但也拥有自己的一部分数据:线程ID、一组寄存器、栈、errno、信号屏蔽字、调度优先级。

3、一进程的多个线程共享
同一地址空间,因此Text Segment、Data Segment都是共享的。如果定义一个函数,在各线程中都可以调用;如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:
1)文件描述符表;
2)每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数);
3)当前工作目录
4)用户ID和组ID

4、进程和线程的关系如下图:
这里写图片描述

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

6、线程的缺点:
(1)性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与供它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

(2)健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

(3)缺乏访问控制:
进程是访问控制的基本力度,在一个线程中调用某些OS函数会对整个进程造成影响。

(4)编程难度提高:
编程与调试一个多进程程序比单线程程序困难多。

二、线程控制
1、POSIX线程库
(1)与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“thread_”打头的。
(2)要使用这些函数库,要通过引入头文。
(3)链接这些线程函数库时要使用编译器命令的“-lpthread”选项。

2、创建线程
(1)功能:创建一个新的线程。

(2)原型:

     int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

(3)参数
1)thread:返回线程ID;
2)attr:设置线程的属性,attr为NULL表示使用默认属性;
3)start_routine:是个函数地址,线程启动后要执行的函数;
4)arg:传给线程启动函数的参数。

(4)返回值:成功返回0;失败返回错误码。

(5)错误检查:
1)传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
2)pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做),而是将错误代码通过返回值返回。
3)pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码的函数。对于pthreads函数的错误,建议通过返回值以判定,因为读取返回值要比读取线程内的errno变量的开销更小。

【例】

#include<unistd.h>
#include<stdio.h>
#include<pthread.h>
#include<string.h>
#include<stdlib.h>

void *rout(void *arg){
    int i;
    for( ; ; ){
        printf("I am pthread 1\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);
    }

    int i;
    for( ; ; ){
        printf("I am main thread\n");
        sleep(1);
    }
}   

运行情况:
这里写图片描述

三、进程ID和线程ID
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)进程和线程的区别如下图所示:
这里写图片描述

(3)线程组的查看:
现在介绍的线程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);

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

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

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

猜你喜欢

转载自blog.csdn.net/m0_38121874/article/details/79458469