Linux:详解多线程(线程概念、线程控制—线程创建、线程终止、线程等待)(一)


1. 线程

1.1 线程概念

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

总而言之,线程就是一个执行流,并且一个进程当中一定存在一个主线程,执行main函数的线程就被称为主线程,其余线程为工作线程。

用一张图来解释就是:

在这里插入图片描述
也就是说:
之前说到的进程,本质上是一个线程组,换句话来说一个线程组可以称之为一个进程。
线程也可以称之为轻量级进程(LWP),在操作系统内核中,是压根就没有线程的概念的。

我们再进入到Linux源码中查看PCB,也就是struct task_struct结构体的定义,有一个pidtgid的变量。

在这里插入图片描述

  • pid:轻量级进程id,也被称之为线程id。
  • tgid:轻量级进程组id,也被称之为进程id。

因此就有以下几个结论:

  • 在一个进程中,不管这个进程有多少个线程,在所有的线程的PCB中,tgid都是相等的
  • 主线程(执行mian函数的LWP)的pidtgid相等。
  • 除了主线程,工作线程的pid,都是不一样的,可以用pid去区分到底是哪一个线程。

1.2 线程的共享与独有

我们在上面所画的图中也提到了,工作线程是拷贝于主线程的,它们PCB中的内存指针均指向同一份进程虚拟地址空间,相当于vfork函数一样,那么线程是否存在和vfork函数一样的调用栈混乱问题呢?

所谓调用栈混乱,就是指有两个指向同一个进程虚拟地址空间的进程,当进程调用某个函数时,相当于将该函数入栈等待执行,现在进程A和进程B执行不同的函数,当进程A的函数入栈等待执行时,正在执行进程B的函数是一个死循环,会导致进程A的函数一直等待出栈,后来被调用的函数也会一直入栈,而不会出栈,这就造成了调用栈混乱。

那么线程中是否存在这样的问题呢?

答案是否定的,还记得进程虚拟地址空间中有一块共享区吗?每一个线程在创建的时候,都会在共享区中创建一份属于自己的调用堆栈、寄存器、线程id等等的信息,这样在执行某个线程的时候,该线程只需要在共享区中拿到属于自己的那一份相关信息进行操作即可,并不会造成调用栈混乱的问题。

用图来解释就是:

在这里插入图片描述

线程的独有:

如上图所示,线程在共享区中有属于自己的调用堆栈、寄存器、线程ID、errno、信号屏蔽字、调度优先级,等等。

线程的共享:

线程之间的共享有:文件描述符表(即fd_array[]数组)、当前进程工作目录、用户 id 和用户组 id、信号的处理方式。

1.3 线程的优缺点

在讲线程的优缺点之前,我们必须先了解并行和并发的概念。

  • 并行:每个执行流在同一时间内都拥有不同的CPU,同时进行运算。
  • 并发:多个执行流在同一时刻只能有一个执行流拥有CPU进行运算。

1.3.1 线程的优点

  • 一个进程当中多个执行流可以并行的执行代码,这样就可以提高程序的运行效率。
  • 进程切换要比线程切换操作系统的代价大。
  • 线程占用的资源要比进程少很多。
  • 能充分利用多处理器的可并行数量。
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

1.3.2 线程的缺点

  • 性能损失

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

  • 健壮性降低(鲁棒性)

    编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
    所谓健壮性是指描述代码运行稳定的词语。

  • 缺乏访问控制

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

  • 编程难度提高

    编写与调试一个多线程程序比单线程程序困难得多。

还有一个缺点就是,在一个进程当中,当线程的数量远远超过CPU数量的时候,有可能线程切换的开销会影响到程序的运行效率,因此,程序当中的线程数量并不是越多越好。

但要是在面试过程中面试官问你,一个程序中线程数量最多到底有多少呢?你该如何回答?

这个答案是不能确定的,因为在不同的机器上,对运算程序的承载能力都是有一个阈值的,在超过该阈值的时候,程序的运行效率就有可能会大大的降低,那么该如何求该阈值呢?可以通过压力测试,在高并发的情况下,我们应该能从结果中得到一个类似于正态分布的函数图像,在该图像的最高点,就是我们所要求的阈值,也就是我们想要的最多的线程数量。
在这里插入图片描述

1.4 线程与进程的对比

前言:

① 线程是操作系统调度的基本单位。
② 进程是操作系统资源分配的基本单位。

对比:

①进程的健壮性要比线程好。
②多线程要比多进程耗费资源小,并且切换快,程序的效率高。

多线程的应用场景有:shell、守护进程、分布式服务

还记得守护进程吗?守护进程是一个生存周期较长的进程,通常独立于控制终端并且周期性的执行某种任务或者等待处理某些待发生的事件,通俗的来说就是可以用该进程实现7*24小时服务不间断。它可以对它创建出来的子进程进行监控,当发现子进程出现异常的时候,就会立即重新拉起一个子进程并且结束该进程。

那么它是怎样进行监控的呢?通过进程间通信即可实现,每当它创建出一个子进程,均会创建一块共享内存来实现进程间通信,子进程每隔一段时间会向共享内存写入一段信息,用来表示自己正在运行,比如这里写入的是当前系统的时间,而守护进程也会每隔一段时间从共享内存中读取信息用来做对比若是每次得到的系统的时间均是不同的,则表明该子进程在正常运行,若对比结果是和上一次读取的时间相同,守护进程不会立即重新拉起子进程,而是可能会再从共享内存中读取,若是连续读取3次,发现结果均相同,则说明该子进程出问题了,则会立即重新拉起一个子进程来代替当前进程(这里验证的方法会有很多种,我只是在这举了一种例子)。

那么,一个大型的服务器的运行,就单靠一个守护进程就能实现7 * 24小时不间断服务吗?不说别的,就说 守护进程在重新拉起一个子进程去替换当前子进程的时候,这个时间段内,程序是停止服务的,也就不存在所谓7 * 24小时不间断了。

那么,该如何实现7 * 24小时不间断服务呢?

答案是使用分布式服务。所谓分布式服务就是一个业务分拆多个子业务,部署在不同的服务器上;就拿微信来举例子,假设微信的服务器设立在上海、北京、深圳等地方,这些不同地方的服务器均能实现微信程序相应的功能,通过使用nginx实现的负载均衡和路由转发就可以解决当某个进程出现异常的时候,在重新拉起新的进程去替换该异常进程的时间段内不会停止服务。**因为nginx实现的负载均衡会根据服务器的状况,按照相应的比例将收到的请求转发出去,当他检测到有一个服务器出现异常的时候,就会将请求分配到其他服务器上去,直到该异常服务器恢复正常,**这样就真正的实现了所谓的7 * 24 小时不间断服务。

2. 线程控制

线程相关的操作函数,均在#include<pthread.h>中包含

2.1 创建线程

2.1.1 线程创建的接口

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

参数:

  • pthread_t:线程的标识符,本质上是线程在共享区独有的空间首地址。
  • thread:是一个出参,该值是由pthread_create函数赋值的。
  • pthread_attr_t:创建线程的属性,一般情况下都指定为NULL,由采取默认的属性
  • void*(*start_routine)(void *):函数指针,接收一个返回值为void*,参数为void*的函数地址,换句话来说,就是线程的入口函数。
  • void* arg:给线程入口函数传递参数。由于参数类型为void*,所以给了程序无限的传递参数的形式,可以传char* 、int*、结构体指针、this指针,等等。

返回值:

  • 0:创建成功
  • <0:创建失败(且返回的值是一个错误码)

注意:在编译多线程的程序时,一定要链接其对应的动态库libpthread.so

2.1.2 线程创建的代码验证

我们使用pthread_create函数创建出一个线程,并给其对应的线程入口函数传入的一个数字,并在该入口函数中对其进行打印。

代码如下:

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

void* pthreadEntry(void* arg)
{
    int* t = (int*)arg;
    while(1)
    {
        printf("I'm create pthread,my arg is %d\n",*t);
        sleep(1);
    }

}

int main()
{
    pthread_t pt;
    int i = 4;
    int ret = pthread_create(&pt,NULL,pthreadEntry,(void*)&i);
    if(ret < 0)
    {
        perror("pthread_create");
        return 0;
    }
    //create success
    while(1)
    {
        puts("i am main pthread");
        sleep(1);
    }
    
    return 0;
}

运行结果如下:

在这里插入图片描述
我们使用ps aux | grep [xxx]命令来查询当前进程的进程号,再通过pstack [PID]命令查看当前进程调用堆栈的信息。
在这里插入图片描述
对调用的栈进行分析,可得以下信息:
在这里插入图片描述
在这里,我们介绍一种命令top -H -p [PID],他可以查看我们当前进程中线程得运行情况。
在这里插入图片描述
我们可以清晰的查看线程的一些信息

但是上面的写的代码是有问题的,我们给线程入口函数传入的参数是int i = 4的地址,而i是一个局部的变量,是在栈上开辟的一块内存,它的作用域在main()函数中,假设当main函数结束后,它的作用域就会被释放,它所在的内存也会被释放,那么就会出现非法访问的错误,这里程序还能正常运行是我们的main函数它没有结束,它的那块内存还没有被释放,线程可以根据传入的地址访问到i这块内存的空间。

因此,我们要搞清楚的是,给线程入口函数传入的arg参数必须是在堆上开辟的变量,或者是一个全局变量,如果传入的是一个局部的,在栈上开辟空间存储的变量,那么传递给线程就可能会有越界访问的错误存在。

代码改进:
现在,我们在主线程中使用一个for循环,循环4次,每次循环创建一个线程,并为该线程传递一个结构体类型的参数,该结构体中只保存一个变量,用来存储循环的次数。

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

typedef struct mytest{
    int i;
}mytest;

void* pthreadEntry(void* arg)
{
    mytest* t = (mytest*) arg;
    while(1)
    {
        printf("I'm create pthread,my arg is %d\n",t->i);
        sleep(1);
    }
    free(arg);

}

int main()
{
    pthread_t pt;
    for(int i = 0; i < 4; ++i)
    {
        mytest* t = (mytest*)malloc(sizeof(mytest));
        t->i = i;
        int ret = pthread_create(&pt,NULL,pthreadEntry,(void*)t);
        if(ret < 0)
        {
            perror("pthread_create");
            return 0;
        }

    }

    //create success
    while(1)
    {
        puts("i am main pthread");
        sleep(1);
    }

    return 0;
}

运行结果:

在这里插入图片描述
在这里插入图片描述
到此为止,线程创建才算万无一失!!!

2.2 线程终止

2.2.1 线程终止的三种情况

pthread_exit函数

void pthread_exit(void *retval);

参数:

  • retval:在线程A结束的时候,传递给等待线程B的参数。

作用:谁调用,谁退出。

pthread_cancel函数

int pthread_cancel(pthread_t thread);

参数:

  • thread:被终止的线程标识符。
  • 获取自己的标识符,可以使用pthread_self()函数。

看参数就可以明白,他可以终止别的线程,只要拥有对应的线程标识符即可,一般都是在主线程中调用。

返回值:

如果等于0,则说明线程终止成功,如果小于0,则说明失败了。

线程的入口函数代码执行完毕之后,线程就退出了。

2.2.3 线程退出的代码验证

现在我们创建出2个工作线程,并在线程入口函数中打印出自己的对应的线程序号,然后分别测试调用pthread_exit函数和pthread_cancel函数来进行线程终止。

代码如下:

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

#define PTHREADNUM 2

void* Mypthreadcallback(void* arg)
{
    int* tmp = (int*)arg;
    puts("pthread_exit test!!");
    //pthread_exit(NULL);
    //pthread_cancel(pthread_self());
    while(1)
    {
        printf("i am work thread ,my thread is %d\n",*tmp); 
        sleep(3);
    }
    delete tmp;
    return NULL;
}

int main()
{
    pthread_t tid[PTHREADNUM];

    for(int i = 0; i < PTHREADNUM;++i)
    {
        int* tmp = new int(i+1);
        int ret = pthread_create(&tid[i],NULL,Mypthreadcallback,tmp);
        if(ret < 0)
        {
            perror("pthread_create");
            return 0;
        }
    }
    puts("pthread_cancel test start!!");
    puts("It's test to cancel work thread 1");
    int ret = pthread_cancel(tid[0]);
    if(ret < 0)
    {
        perror("pthread_cancel");
        return 0;
    }
    sleep(2);
    puts("It's test to cancel work thread 2");
    ret = pthread_cancel(tid[1]);
    if(ret < 0)
    {
        perror("pthread_cancel");
        return 0;
    }
    while(1){

        puts("i am main thread");
        sleep(1);
    }

    return 0;
}

运行结果如下:

在这里插入图片描述

需要注意的是,在线程退出时一定要考虑有哪些资源没有被释放。

如果在主线程代码中调用pthread_cancel函数取消自己,则主线程的状态会变为僵尸状态,而整个工作线程是正常的,整个进程并没有退出。

测试代码如下:

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

#define PTHREADNUM 2

void* Mypthreadcallback(void* arg)
{
    while(1)
    {
        printf("i am work thread\n"); 
        sleep(1);
    }
    return NULL;
}

int main()
{
    pthread_t tid;

    int ret = pthread_create(&tid,NULL,Mypthreadcallback,NULL);
    if(ret < 0)
    {
        perror("pthread_create");
        return 0;
    }
    puts("pthread_cancel test start!!");
    puts("It's test to cancel main thread");
    ret = pthread_cancel(pthread_self());
    if(ret < 0)
    {
        perror("pthread_cancel");
        return 0;
    }

    while(1){
        sleep(1);
    }

    return 0;
}

运行结果如下:

在这里插入图片描述
我们发现,明明主线程都已经退出了,而工作线程却还在执行,这是为什么呢?
在这里插入图片描述

这就验证了如果在主线程代码中调用pthread_cancel函数取消自己,则主线程的状态会变为僵尸状态,而整个工作线程是正常的,整个进程并没有退出。

2.3 线程等待

2.3.1 线程等待的原因

由于线程的默认属性为joinable属性,当线程退出的时候,其资源并不会被操作系统回收,(这个的资源指的是进程虚拟地址空间共享区中每个线程所对应的那个空间),它需要其他线程来进行线程等待,继续回收,否则就会发生内存泄漏。

2.3.2 线程等待的接口

int pthread_join(pthread_t thread, void **retval);

参数:

  • thread:需要进行等待的线程标识符。
  • retval:线程退出时的返回值。
    ①若为线程入口函数退出时,retval就是线程入口函数的返回值。
    ②若为pthread_exit函数,则它接收到的就是该函数的retval参数。
    ③若为pthread_cancel函数,则它的值就是一个常数,该常数为:PTHREAD_CANCELED

返回值:

如果等于0,则说明线程终止成功,如果小于0,则说明失败了。

调用pthread_join函数进行等待的执行流,如果还没有等待到退出的线程,则当前调用pthread_join函数的执行流就会被阻塞。

2.3.3 线程等待的代码验证

创建出一个线程,并在该线程中调用pthread_cancel函数终止掉自己,在主线程中对该工作线程进行等待。

代码如下:

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

#define PTHREADNUM 2

void* Mypthreadcallback(void* arg)
{
    while(1)
    {
        printf("i am work thread\n"); 
        sleep(1);
        pthread_cancel(pthread_self());
    }
    return NULL;
}

int main()
{
    pthread_t tid;

    int ret = pthread_create(&tid,NULL,Mypthreadcallback,NULL);
    if(ret < 0)
    {
        perror("pthread_create");
        return 0;
    }

    puts("pthread_join test start!!");
    void *retval;
    ret = pthread_join(tid,&retval);
    if(ret < 0)
    {
        perror("pthread_join");
        return 0;
    }

    printf("pthread_join success!,It's retval is %p\n",retval);

    while(1){
        puts("i am main thread");
        sleep(1);
    }

    return 0;
}

运行结果如下:

在这里插入图片描述

2.4 线程分离

2.4.1 原理

一个线程如果从joinable属性变为detach属性,则当前线程在退出的时候,不需要其他线程来回收资源(即不需要进行相应的线程等待),操作系统会自己进行资源的回收。

2.4.2 线程分离的接口

int pthread_detach(pthread_t thread);

参数:

  • thread:待要分离的线程描述符

返回值:

如果等于0,则说明线程终止成功,如果小于0,则说明失败了。

  • 15
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值