1

初始多线程 - MElephant

 1 year ago
source link: https://www.cnblogs.com/hyacinthLJP/p/16756538.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

初始多线程

一、基本概念

1.1 应用程序

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

1.2 进程

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

1494888-20221006103949527-1155470672.png

进程的特点:

  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;它们在时间上是不可能发生重叠的。

1494888-20221006103949257-1284346919.png

2.1.2 并行

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

1494888-20221006103948997-756674217.png

2.2 多线程

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

1494888-20221006103948608-1912330098.png

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

所谓多线程,即一个进程中拥有多个线程(≥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 <pthread.h>

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

参数介绍

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

返 回 值

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

3.2.2 牛刀小试

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

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

/* 线程执行函数 */
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。

运行结果如下:

1494888-20221006103948189-421760466.png

上述代码中,我们通过在 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 <stdio.h>
#include <pthread.h>

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 <pthread.h>

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

参数介绍

  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;
}

运行结果:

1494888-20221006103947836-2009934456.png

3.3.5 及时释放资源

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

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

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

#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <string.h>

/* 线程函数,不作任何操作 */
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;
}

运行结果如下:

1494888-20221006103947391-1138724769.png

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

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

3.4 pthread_detach()

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

头 文 件:#include <pthread.h>

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

参数介绍:线程标识符

返 回 值

  • 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 <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <string.h>

/* 线程函数,不作任何操作 */
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__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK