线程详解

线程概念

线程是一个进程内部的控制序列,是程序里的一个执行线路
一切进程至少都有一个执行线程,线程在进程内部运行,本质上是在进程的地址空间内运行。在CPU眼中,看到的PCB要比传统的进程更加轻量化。透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

总而言之,线程是在进程内部运行的一个执行流,Linux系统没有真正意义上的线程,而是用PCB模拟的。进程是承担分配系统资源的基本实体,线程是调度的基本单位。在线程组里面,所有的线程都是对等的关系,没有父线程的概念。

什么是主线程?

主线程是创建在进程中产生的第一个线程,也就是main函数对应的线程。

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

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

线程异常
如果单个线程出现除零、野指针等问题导致线程崩溃,进程也会随之崩溃。线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

Linux进程VS线程

进程是资源分配的基本单位,而线程是调度的基本单位。
线程共享进程数据,但也拥有自己的一部分数据:

  • 线程ID
  • 一组寄存器
  • 栈(每个线程都有自己独立的栈结构,每个线程都有自己的硬件上下文,体现线程的调度)
  • error信息
  • 信号屏蔽字(一个进程中pending(未决)信号只有一个,但任意一个线程都可以处理这个信号)
  • 调度优先级

进程的多个线程共享同一个地址空间,如果定义了一个函数,在个线程中都可以调用,如果定义了一个全局变量,在个线程中都可以访问到。各线程还可以共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
  • 当前的工作目录
  • 用户id和组id
进程与线程的区别
  • 进程是资源分配的最小单元,线程是CPU调度的最小单元。
  • 进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段。线程没有独立的地址空间,它使用相同的地址空间共享数据。线程拥有自己的一部分数据是:线程ID、一组寄存器、栈结构、error错误信息、信号屏蔽字、调度优先级。但进程也有自己的私有属性,比如进程控制块PCB,这些私有属性是不被共享的,用来标识一个进程
  • CPU切换一个线程比切换进程的花费小,创建一个线程比进程开销小。线程占用的资源比进程少很多。
  • 线程之间通信更方便,同一个进程下,线程共享全局变量、静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行,多线程需要处理好同步与互斥。
  • 多进程程序更安全,一个进程死掉不会对另一个进程造成影响(这源于每个进程有独立的地址空间)。多线程程序更不容易委会,一个线程挂掉,整个程序就挂了(因为它们共享地址空间)。
  • 进程对资源保护要求更高,开销大,效率相对较低。线程资源保护要求不高,但开销小,效率高,可频繁进行线程切换。
线程控制

线程创建

函数功能:创建一个新线程
int pthread_create(pthread_t *thread,const pthread_attr,void *(start_routine)(void,void *arg));
参数:thread:返回线程id
attr:设置线程属性,为NULL表示线程默认属性
start_routine:函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数,这里设置为空,不带参数内容
返回值:成功返回0,失败返回错误码

#include<iostream>
#include<unistd.h>
#include<string>
#include<pthread.h>
#include<sys/types.h>

void *thread_routine(void* arg){
	string str = (char*) arg;
	while(1){
		cout << str << "run" << "pid:" <<getpid() << endl;
		sleep(1);
	}
}

int main(){
	pthread_t tid;
	pthread_create(&tid,NULL,thread_routine,(void*)"thread 1");
	pthread_create(&tid,NULL,thread_routine,(void*)"thread 2");
	pthread_create(&tid,NULL,thread_routine,(void*)"thread 3");
	pthread_create(&tid,NULL,thread_routine,(void*)"thread 4");
	while(1){
		cout << "main thread run" << "pid:" << getpid() << endl;
		sleep(2);
	}
	return 0;
}

运行结果
在这里插入图片描述

线程ID和进程ID

在Linux中,目前的线程实现是Native POSX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process,LWP),每个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。

没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。引入线程概念后,一个用户进程下管辖了N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符就变成了1:N的关系。POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决这个问题呢?Linux就引入了线程组的概念。

多线程

多线程的进程又称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表面上看对应的是进程id,其实不然,它对应的是线程id进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程id。
在这里插入图片描述
这里的线程ID不同于pthread_t类型的线程ID,和进程ID一样,线程ID是pid_t类型的变量,而且是用来唯一标识线程的一个整形变量。查看一个线程的ID用命令:
在这里插入图片描述
ps命令中的-L选项会显示如下信息:

LWP:线程ID,即是gettid()系统调用的返回值
NLMP:线程组内线程的个数

用户线程和内核线程的区别

LWP表示的是轻量级进程,由操作系统提供。NLWP是线程组内线程的个数。
pthread是用户级别的库,它的pid本质上是一个地址,即pthread动态库中结构体的起始地址,与LWP中的pid有一一对应的关系。
可以看出上面的./PthreadCreate是多线程的,进程id为14885,进程内有两个线程,线程ID分别为14885和14886.
./PthreadCreate进程的id为14885,下面有一个线程的id也是14885,这并不是巧合。线程组内的第一个线程,在用户态被称为主线程(main thread),在内核态中被称为group_ leader,内核在创建第一个线程时,会将线程组的id值设置成第一个线程的线程id,group_ leader指针则指向自身,即主线程的进程描述符。至于线程组其他线程的ID则由内核负责分配,其线程组id总是和主线程的线程组ID一致,无论是主线程直接创建线程,还是创建出来的线程再次创建线程都是如此。
值得注意的是:pthread_create会产生一个线程ID,存放在第一个参数指向的地址中。该线程id和前面提到的线程ID不是一回事。前面说的线程ID属于进程调度的范畴(内核线程),因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一标识这个线程。而pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID属于NPTL线程库的范畴(用户线程)。线程的后续操作,就是根据该线程ID来操作线程的。

NPTL线程库还提供了pthread_self函数,可以获得线程自身的ID:pthread_t pthread_self(void);

因此对于Linux目前的NPTL实现而言,pthread_t类型的线ID本质上就是一个进程地址空间上的一个地址。
在这里插入图片描述

线程的终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

1.从线程函数return,这种方法对主线程不适用,从main函数return相当于调用了exit。在新线程中不能用exit,因为这个进程就退出了。

2.线程可以调用pthread_exit终止自己。

3.一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

pthread_exit函数

功能:终止线程
原型:
void pthread_exit(void *retval);
参数:
retval是线程退出时返回给主线程的值。
返回值:
无返回值,跟进程一样,线程结束的时候无法返回到它的调用者

需要注意的是:pthread_exit或return返回的指针所指向的内存单元必须是全局的或是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。

pthread_cancel函数

功能:取消一个执行中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:
thread:表示线程id
返回值:
成功返回0,失败返回退出码

线程等待

为什么需要线程等待?

保证线程的退出顺序:保证一个线程退出并且回收资源后允许下一个进程退出
回收线程退出时的资源情况:保证当前线程退出后,创建的新线程不会复用刚才退出线程的地址空间
获得新线程退出时的结果是否正确的退出返回值,这个有点类似回收僵尸进程的wait,保证不会发生内存泄露等问题

pthread_join函数

在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到pthread_join()方法了。
即pthread_join()的作用可以这样理解:主线程等待子线程的终止。也就是在子线程调用了pthread_join()方法后面的代码,只有等到子线程结束了才能执行。

功能:阻塞等待线程退出,获取线程退出状态
原型:
    int pthread_join(pthread_t thread,void **value_ptr);
参数:
    thread:线程id
    value_ptr:它指向一个指针,指向等待线程的返回值
返回值:
    成功返回0,失败返回错误码

需要注意的是:pthread_exit或return返回的指针所指向的内存单元必须是全局的或是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。

如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。
如果thread线程被别的线程调用pthread_cancel异常终止掉了,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数
如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数

编写代码说明线程的创建、退出、等待

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<sys/types.h>

void* thread1(void* x)
{
    printf("thread1_return : pid = %u, tid = %u\n", getpid(), (unsigned int)pthread_self());
    return (void*)1;
}

void* thread2(void* x)
{
    printf("thread2_exit : pid = %u, tid = %u\n", getpid(), (unsigned int)pthread_self());
    pthread_exit((void*)2);
}

void* thread3(void* x)
{
    printf("thread3_cancel : pid = %u, tid = %u\n", getpid(), (unsigned int)pthread_self());
    while (1)
    {
        printf("thread3 needs to be cancelled\n");
        sleep(1);
    }
}


int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;

    void* ret1;
    void* ret2;
    void* ret3;

    pthread_create(&tid1, NULL, thread1, NULL);
    pthread_join(tid1, &ret1);
    printf("thread %u, return code is %d\n", (unsigned)tid1, (int)ret1);

    pthread_create(&tid2, NULL, thread2, NULL);
    pthread_join(tid2, &ret2);
    printf("thread %u, exit code is %d\n", (unsigned)tid2, (int)ret2);


    pthread_create(&tid3, NULL, thread3, NULL);
    sleep(3);
    pthread_cancel(tid3);
    pthread_join(tid3, &ret3);
    printf("thread %u, cancel code is %d\n", (unsigned)tid3, (int)ret3);

    return 0;
}
线程分离

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这时,我们可以告诉系统,当线程退出时,自动释放线程资源。用到的函数是pthread_detach

pthread_detach函数
int pthread_detach(pthread_t thread);    

此函数的作用是从状态上实现线程分离,注意不是指该线程独自占用地址空间。线程分离可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable,又是分离的。

为什么有时候需要线程分离?

pthread_detach用于分离可结合线程tid。线程能够通过以pthread_self()为参数的pthread_detach调用来分离它们自己。如果一个可结合线程结束运行但没有被join,那么它的状态类似于进程中的僵尸进程,即还有一部分资源没有被回收,所以创建线程者应该调用pthread_join来等待线程运行结束,并可得到线程的退出码,回收资源。由于调用pthread_join后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此。例如,在Web服务器中主线程为每个新来的连接请求创建一个子进程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的连接请求),这是可以在子线程中加入代码pthread_detach(pthread_self()),这样一来,该线程运行结束之后会自动释放所有资源。在线程中使用pthread_detach之后就不能使用pthread_join了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值