• 初始多线程


    初始多线程

    一、基本概念

    1.1 应用程序

    以 Windows 为例,一个拓展名为 .exe 的文件就是一个应用程序,应用程序是能够双击运行的。

    1.2 进程

    应用程序运行起来就创建了一个进程,即进程就是运行起来的应用程序;如电脑上运行的 Edge、Typora、PotPlayer 等。

    image-20221004001922323

    进程的特点:

    1. 一个进程至少包含一个线程(主线程,main)。
    2. 可以包含多个线程(主线程+若干子线程)。
    3. 所有线程共享进程的资源。

    1.3 线程

    1.3.1 线程概念

    我们知道,一个进程指的是一个正在执行的应用程序;而线程则是执行进程中的某个具体任务,比如一段程序、一个函数等。

    进程想要执行任务就需要依赖线程;换句话说,线程就是进程中的最小执行单位,并且一个进程中至少有一个线程(主线程)。

    1.3.2 主线程

    1. 每个进程都有一个主线程,这个主线程是唯一的。
    2. 当你运行了一个应用程序产生了一个进程后,这个主线程就随着这个进程默默地启动起来了。
    3. 主线程的生命周期与进程的生命周期相同,它俩同时存在、同时结束,是唇齿相依的关系。
    4. 一个进程只能有一个主线程,就像一个项目中只能有一个 main 函数一样。

    1.4 进程和线程的关系

    线程和进程之间的关系,类似于工厂和工人之间的关系:

    • 进程好比是工厂,线程就如同工厂中的工人。
    • 一个工厂可以容纳多个工人,工厂负责为所有工人提供必要的资源(电力、产品原料、食堂、厕所等),所有工人共享这些资源。
    • 每个工人负责完成一项具体的任务,他们相互配合,共同保证整个工厂的平稳运行。

    进程仅负责为各个线程提供所需的资源,真正执行任务的是线程,而不是进程。

    二、多线程概念

    提到多线程这里要说两个概念,就是串行和并行,搞清楚这个,我们才能更好地理解多线程。

    2.1 串行和并行

    2.1.1 串行

    所谓串行,其实是相对于单条线程来执行多个任务来说的,我们就拿下载文件来举个例子:当我们下载多个文件时,在串行中它是按照一定的顺序去进行下载的,也就是说,必须等下载完 A 之后才能开始下载 B;它们在时间上是不可能发生重叠的。

    image-20221004002625501

    2.1.2 并行

    下载多个文件,多个文件同时进行下载;这里是严格意义上的,在同一时刻发生的,并行在时间上是重叠的。

    image-20221004002744435

    2.2 多线程

    了解了串行和并行这两个概念之后,我们再来说说什么是多线程。举个例子,我们打开腾讯管家,腾讯管家本身就是一个应用程序,也就是说它就是一个进程,它里面有很多的功能,我们可以看下图,能查杀病毒、清理垃圾、电脑加速等众多功能:

    image-20221005215001689

    按照单线程来说,无论你想要清理垃圾、还是要病毒查杀,那么你必须先做完其中的一件事,才能做下一件事,这里面是有一个执行顺序的。如果是多线程的话,我们其实在清理垃圾的时候,还可以进行查杀病毒、电脑加速等等其他的操作,这个是严格意义上的同一时刻发生的,没有执行上的先后顺序。

    所谓多线程,即一个进程中拥有多个线程(≥2,主线程+若干子线程),线程之间相互协作、共同执行一个应用程序。

    三、多线程编程

    我们通常将以「多线程」方式编写的程序称为「多线程程序」,将编写多线程程序的过程称为「多线程编程」,将拥有多个线程的进程称为「多线程进程」。

    PS:以下代码是在 Linux 下运行的。

    3.1 pthread_t

    定义:typedef unsigned long int pthread_t;

    功能:用于声明线程ID,是一个线程标识符。

    3.2 pthread_create()

    3.2.1 函数介绍

    函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

    头 文 件:#include

    功能介绍:用来创建一个线程

    参数介绍

    1. 第一个参数为指向线程标识符的指针
    2. 第二个参数用来设置线程属性,一般置为 NULL,表示使用默认属性
    3. 第三个参数是线程执行的函数,返回值为 void *
    4. 最后一个参数是线程执行函数的参数

    返 回 值

    • 当创建线程成功时,函数返回0
    • 若不为 0 则说明创建线程失败,常见的错误返回代码为 EAGAIN 和 EINVAL:
      • 前者表示系统限制创建新的线程,例如线程数目过多了
      • 后者表示第二个参数代表的线程属性值非法

    3.2.2 牛刀小试

    下面我们通过代码来深入理解如何创建一个子线程。

    #include 
    #include 
    #include 
    
    /* 线程执行函数 */
    void *func(void *arg)
    {
        int i;
        for (i = 1; i <= 10; i++) //该函数执行动作:打印 10 次 func
        {
            printf("func[%d]\n", i);
        }
        return NULL;
    }
    
    int main()
    {
        printf("主线程开始运行\n");
    
        pthread_t th; //定义一个线程标识符
    
        /* 使用默认属性创建线程,该线程执行 func 函数 */
        if (0 != pthread_create(&th, NULL, func, NULL))
        {
            /* 输出错误日志并打印相应的错误码 */
            printf("fail, errno[%d, %s], \n", errno, strerror(errno));
        }
    
        /* 阻塞主线程,不然,主线程马上结束,从而使创建的线程没有机会开始执行就结束了 */
        sleep(2);
        
        printf("主线程结束运行\n");
        return 0;
    }

    注意:编写 Linux 下的多线程程序时,需要使用头文件pthread.h,连接时需要使用静态库 libpthread.a。

    运行结果如下:

    image-20221004150207736

    上述代码中,我们通过在 main 中添加 sleep 来阻塞主线程,以此保证子线程可以正常运行并终止;但通过 sleep 的方式阻塞主线程多少会影响程序效率,所以我们需要换一种方式来阻塞主线程。

    3.3 pthread_join()

    3.3.1 小栗子

    在讲解 pthread_join 前,我们先来通过一个小栗子初步体验一下为何需要 pthread_join。

    场景 1

    在简单的程序中一般只需要一个线程就可以搞定,也就是主线程:

    int main()
    {
        printf("主线程开始运行\n");
    
        return 0;
    }

    现在假设我要做一个比较耗时的工作,从一个服务器下载一个视频并进行处理,那么我的代码会变成:

    int main()
    {
        printf("主线程开始运行\n");
        download(); // 下载视频到本地
        process();  // 视频处理
        
        return 0;
    }

    场景 2

    如果我需要下载两个视频素材,一起在本地进行处理,也很简单:

    int main()
    {
        printf("主线程开始运行\n");
        download1();    //下载视频 1
        download2();    //下载视频 2
        process();      //处理视频 1、2
    
        return 0;
    }

    本身这么做完全没有问题,可是就是有点浪费时间,如果两个视频能够同时下载就好了,这时候线程就派上了用场。

    #include 
    #include 
    
    void *download1(void *arg)
    {
        puts("子线程开始下载第一个视频...");
        sleep(6);  // 耗时 6s
        puts("第一个视频下载完成");
    }
    void *download2(void *arg)
    {
        puts("主线程开始下载第二个视频...");
        sleep(10);  // 耗时 10s
        puts("第二个视频下载完成");
    
        return NULL;
    }
    void process()
    {
        puts("开始处理两个视频...");
        sleep(3);  // 耗时 3s
        puts("处理完成");
    }
    int main()
    {
        printf("主线程开始运行\n");
    
        pthread_t th;
        pthread_create(&th, NULL, download1, NULL);     // 子线程下载视频 1
    
        download2(NULL);                                // 主线程下载视频 2
        
        process();//处理视频1、2
    
        return 0;
    }

    主线程叫来了 th 这个线程去下载「视频 1」,自己去下载「视频 2」;减轻了自己的工作量也缩短了时间。

    通过download函数的对比,可以发现,两个视频同时下载肯定是「视频 1」先下载完,这样在主线程下载完「视频 2」的时候,「视频 1」已经准备好了,后面就可以一起进行处理,这没什么问题。

    但是万一「视频 1」的下载时间比「视频 2」的时间长呢(比如下载「视频 2」仅需要耗费 3s 的时间)?当「视频 2」下载完成了,但此时子线程 th 还没干完活,本地还没有「视频 1」,那么接下来处理的时候肯定会有问题,或者说接下来不能直接进行处理,要等 th 干完活后,主线程中的process函数才能去处理这两个视频。

    在这种场景下就用到了pthread_join()这个函数。

    3.3.2 pthread_join介绍

    函数原型:int pthread_join(pthread_t thread, void **retval);

    头 文 件:#include

    功能介绍:用来等待一个线程的结束

    参数介绍

    1. 第一个参数为被等待的线程标识符
    2. 第二个参数为一个用户定义的指针,它可以用来保存被等待线程的返回值

    返 回 值

    • On success, pthread_join() returns 0
    • On error, it returns an error number

    这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。

    3.3.3 调用 pthread_join

    下面,我们通过 pthread_join 修改一下「场景 2」的代码:

    void *download1(void *arg)
    {
        puts("子线程开始下载第一个视频...");
        sleep(6);  // 耗时 6s
        puts("第一个视频下载完成");
    }
    void *download2(void *arg)
    {
        puts("主线程开始下载第二个视频...");
        sleep(3);  // 耗时 3s
        puts("第二个视频下载完成");
    
        return NULL;
    }
    int main()
    {
        printf("主线程开始运行\n");
    
        pthread_t th;
        pthread_create(&th, NULL, download1, NULL);     // 子线程下载视频 1
    
        download2(NULL);                                // 主线程下载视频 2
        
        pthread_join(th, NULL);                         // 阻塞主线程,直到「视频 1」下载完成
    
        process();//处理视频1、2
    
        return 0;
    }

    现在下载「视频 1」需要 6s,下载「视频 2」需要 3s;当「视频 2」下载完成后要等待「视频 1」下载完成方可一起进行处理,为了实现这个目的,我们在第 24 行加入了pthread_join()

    在这个场景下,我们明确两个事情:

    Q1:谁调用了pthread_join函数?

    • th 这个线程对象调用了pthread_join函数,因此必须等待 th 的下载任务结束了,pthread_join()才能返回。

    Q2:在哪个线程环境下调用了pthread_join函数?

    • th 是在主线程的环境下调用了pthread_join函数的,因此主线程要等待 th 的工作做完,否则主线程将一直处于阻塞状态。

    这里不要搞混的是子线程 th 真正做的任务(下载「视频 1」)是在另一个线程中做的;但是 th 调用pthread_join函数的动作是在主线程环境下做的。

    3.3.4 获取线程任务的返回值

    子线程执行的函数在结束后可能会有返回值:

    #define STRING_LEN_24 24
    
    /* 线程执行函数 */
    void *func(void *arg)
    {
        int i;
        for (i = 1; i <= 10; i++) //该函数执行动作:打印 10 次 func
        {
            printf("func[%d]\n", i);
        }
    
        char *buf = (char *)malloc(STRING_LEN_24);
        strncpy(buf, "The child thread ends", STRING_LEN_24 - 1);
    
        return buf;
    }

    这种情况下,该如何处理呢?还记得pthread_join()函数的第二个参数吗?这个参数就是用来保存线程函数的返回值的:

    int main()
    {
        printf("主线程开始运行\n");
    
        pthread_t th;
    
        if (0 != pthread_create(&th, NULL, func, NULL))
        {
            printf("fail, errno[%d, %s], \n", errno, strerror(errno));
        }
    
        char *buf;
        pthread_join(th, (void **)&buf);
        printf("子线程返回值[%s]\n", buf);
    
        printf("主线程结束运行\n");
        return 0;
    }

    运行结果:

    image-20221004151246605

    3.3.5 及时释放资源

    引入一个新的概念:线程分离(detach)和非分离(join)状态。线程的分离状态决定一个线程以什么样的方式来终止自己。

    在默认情况下线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。也就是说,通过默认属性创建的线程必须要通过调用pthread_join()函数来释放线程资源,换句话说,非分离状态的线程一定要调用pthread_join()函数。

    对于非分离状态的线程,如果不及时调用pthread_join()函数,则会导致资源泄露。下面就通过创建大量非分离状态的线程,但不调用pthread_join()函数来观察会出现什么情况。

    #include 
    #include 
    #include 
    #include 
    
    /* 线程函数,不作任何操作 */
    void *func(void *arg)
    {
        return NULL;
    }
    
    int main()
    {
        int i;
        for (i = 1; ; i++)
        {
            pthread_t th;
            if (0 != pthread_create(&th, NULL, func, NULL))
            {
                printf("fail, errno[%d, %s]\n", errno, strerror(errno));
                break;
            }
            
            printf("pthread create succeed[%d]\n", i);
        }
    
        return 0;
    }

    运行结果如下:

    image-20221005212343247

    通过运行结果可以看出,未及时释放线程导致内存资源耗尽,进而导致线程创建失败。

    但如果在「第 23 行」添加pthread_join(th, NULL);代码,则可以避免这种情况的发生。

    3.4 pthread_detach()

    函数原型:int pthread_detach(pthread_t thread);

    头 文 件:#include

    功能介绍:从状态上实现线程分离

    参数介绍:线程标识符

    返 回 值

    • On success, pthread_detach() returns 0
    • On error, it returns an error number

    在「3.3.5 及时释放资源」时提到了两个概念:线程分离状态和线程非分离状态。默认创建的线程为非分离状态,那么如何设置线程为分离状态呢?有两种方式:

    1. 调用pthread_detach()函数。
    2. 通过pthread_create()函数的第二个参数来设置线程分离。

    一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join()获取它的状态为止(或者进程终止被回收了)。但是线程也可以被置为 detach 状态;如果线程被设置为了分离状态,那么该线程主动与主控线程断开关系。线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放

    #include 
    #include 
    #include 
    #include 
    
    /* 线程函数,不作任何操作 */
    void *func(void *arg)
    {
        return NULL;
    }
    
    int main()
    {
        int i;
        for (i = 1; ; i++)
        {
            pthread_t th;
            if (0 != pthread_create(&th, NULL, func, NULL))
            {
                printf("fail, errno[%d, %s]\n", errno, strerror(errno));
                break;
            }
            pthread_detach(th);//使用 pthread_detach 函数实现线程分离
            printf("pthread create succeed[%d]\n", i);
        }
    
        return 0;
    }

    注意:不能对一个已经处于 detach 状态的线程调用 pthread_join(),这样的调用将返回 EINVAL 错误。

    参考资料


    __EOF__

  • 本文作者: MElephant
  • 本文链接: https://www.cnblogs.com/hyacinthLJP/p/16756538.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    元宇宙安全与著作权相关市场与技术动态:韩国视角
    HCIP实验(05)OSPF综合实验
    Spring Security 实现动态权限菜单方案(附源码)
    OpenCV——总结《车牌识别》
    python特殊函数之__call__函数的作用
    如何选择一款快速可靠的文件自动同步软件?
    MacOS使用clion配置mounriver 工具链
    websocket 请求头报错 Provisional headers are shown 的解决方法
    当线下门店遇上AI:华为云ModelBox携手佳华科技客流分析实践
    优秀的ui设计作品(合集)
  • 原文地址:https://www.cnblogs.com/hyacinthLJP/p/16756538.html