线程概念与控制(中)

news2025/3/31 22:56:10

线程概念与控制(上)https://blog.csdn.net/Small_entreprene/article/details/146464905?sharetype=blogdetail&sharerId=146464905&sharerefer=PC&sharesource=Small_entreprene&sharefrom=mp_from_link我们经过上一篇的学习,接下来,先来好好整理一下:

对上篇的整理

线程的优点

线程在Linux当中就是根据进程模拟实现的,两者在Linux当中都被称为是轻量级进程,每个进程在自己的用户空间当中,每个进程当中的线程只要有自己的独立的地址空间,那么线程就有了对应的自己的资源了。

那么,线程的优点是哪些呢?

创建一个新线程的代价要比创建一个新进程小得多:

因为创建一个线程之前,一定是进程已经存在了,就是资源已经分配了,而线程只需要创建PCB和资源划分就可以,所以代价当然就小了。

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。(进程内的线程切换)

  • 最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。(不需要将CR3寄存器的内容进行保存,页表不用切换)
  • 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。(最主要的影响原因)简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲TLB(快表)会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。(进程切换会导致TLB和Cache失效,下次运行,需要重新缓存!)

线程占用的资源要比进程少很多:因为线程拿到的资源本身就是进程资源的一部分!

能充分利用多处理器的可并行数量:因为线程本质就是CPU进行调度的基本单位,当我们有多个CPU的时候,我们可以创建多线程,使用多线程,让整个系统多CPU并行起来了!

在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。(这个多进程也是可以实现的)

计算密集型(加密,解密,压缩...使用CPU的)应用,为了能在多处理器系统(多CPU)上运行,将计算分解到多个线程中实现。

I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

那是不是线程越多越好呢?

不是的,因为如果是一计算密集型的应用场景,CPU只有两个的话,最合理的创建线程的个数就是两个,有几个CPU就创建几个线程,只有2CPU,非要创建10个线程,那么在实际计算的时候,每一个CPU均摊的是5个,除了在做计算,还在做切换,切换的算力本来可以用在计算上,还创建那么多线程,反而将效率减慢了。(一个人可以带两份饭,没必要两个人一起排队买),但是IO的话,就可以多创建一些,因为IO需要等待。

线程的缺点

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

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

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

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

只要我们代码写得好,这些问题就不是问题了😊

线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃:因为线程是进程的执行分支,线程出异常,就是进程出异常,所以就会触发系统给进程发信号,杀掉该进程,而线程赖以生存的资源,空间,页表...都是属于进行申请的资源,进程都没了,线程也早就没了。
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率。
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。

Linux进程VS线程

进程和线程

在Linux系统中,进程和线程是两个核心概念,它们在操作系统中扮演着不同的角色。

  • 进程是资源分配的基本单位。每个进程都有自己的地址空间和系统资源,如内存、文件描述符等。

  • 线程是调度的基本单位。线程是进程中的一个执行流,是CPU调度和分派的基本单位。

线程共享进程数据,但也拥有自己的部分数据:

  • 线程ID:每个线程都有一个唯一的标识符。

  • 一组寄存器(大部分的)(线程的上下文数据:证明线程是被独立调度的!!!):线程有自己的寄存器集合,用于存储临时数据和状态信息。

  • (栈不就一个吗?线程是一个动态的概念,需要入栈出栈进行临时数据的保存,后面具体说):每个线程都有自己的栈,用于存储函数调用时的局部变量和返回地址。

  • errno:线程有自己的错误号,用于记录最近一次系统调用的错误。

  • 信号屏蔽字:线程可以设置自己的信号屏蔽字,决定哪些信号可以被处理。

  • 调度优先级:线程有自己的调度优先级,影响其在CPU上的调度顺序。

进程的多个线程共享

同一地址空间,因此Text Segment、Data Segment都是共享的。如果定义一个函数,在各线程中都可以调用。如果定义一个全局变量,在各线程中都可以访问。除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表:所有线程共享相同的文件描述符表,可以访问相同的文件。

  • 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数):所有线程共享相同的信号处理方式。

  • 当前工作目录:所有线程共享相同的当前工作目录。

  • 用户id和组id:所有线程共享相同的用户id和组id。

进程和线程的关系如下图:

关于进程线程的问题

如何看待之前学习的单进程?具有一个线程执行流的进程:在单进程模型中,进程只有一个执行流,即只有一个线程。这种模型简单,但缺乏并发能力。在多线程模型中,一个进程可以有多个线程,每个线程可以独立执行,从而实现并发处理。


Linux线程控制

验证之前的理论

在Linux当中,如果我们想要创建多线程,就需要使用一个库来实现创建多线程

这个库就是:pthread_create

在Linux系统中,创建多线程通常使用POSIX线程(Pthreads)库。Pthreads是一个广泛使用的线程库,它提供了一组API来支持多线程编程。pthread_create函数是Pthreads库中用于创建新线程的关键函数。

以下是pthread_create函数的基本用法:(并不是系统调用哦)

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
  • pthread_t *thread:指向线程标识符的指针,用于存储新创建线程的ID。

  • const pthread_attr_t *attr:指向线程属性对象的指针,用于设置线程属性。如果不需要设置特定属性,可以传递NULL

  • void *(*start_routine) (void *):线程开始执行的函数,即线程的入口函数。(返回值为void*,参数位void*的函数指针

  • void *arg:传递给线程入口函数(start_routine)的参数。

注意:线程入口函数start_routine必须符合以下原型:

void *start_routine(void *arg);

其中,arg是传递给线程的参数,start_routine函数的返回值将被存储在void *类型的指针中,可以通过pthread_join函数获取。

我们下面来写一个简单的创建新线程的代码:

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>

void *threadrun(void *args)
{
    std::string name = (const char *)args;
    while (true)
    {
        sleep(1);
        std::cout << "我是新线程: name: " << name << std::endl;
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");

    while (true)
    {
        std::cout << "我是主线程..." << std::endl;
        sleep(1);
    }
    return 0;
}

我们验证之前的结论:threadrun不就是编译后新的一组虚拟地址!!!(根本就不用我们自己去划分虚拟地址)

接下来,我们来编译一下代码:有时候会链接时报错,这是因为pthread_create不是系统调用,但是它不是第三方库,只是在默认情况下,编译器只会链接一些核心标准库(libc),需要在g++编译后添加" -l+库的名称 ".(我这里是不需要)(其实还有历史的原因,在早期的Unix中,多线程编程并不是默认支持的,是被设计成一个可选的扩展)(下面会说其不是系统调用,是库,是系统调用的)

我们可以看到:

不过我们的代码并没有fork,没有多进程,那么有没有可能单进程的情况下,两个死循环一起跑的,在我们之前的代码中是不可能的,但是今天,我们main函数和threadrun函数是同时被调用的的!(这就可以说明是两线程了,那么怎么看到是两线程呢?)

我们可以看到,只有一个进程,而且为其发送9号信号,杀死进程,可以看出信号是两个线程共享的。

我们使用:ps -aL(使用a:展示所有,L:查看线程)可以查看线程状态

所以同一个进程当中可以存在两个执行流。程序名都是test_thread,因为一个执行的是代码中的main,一个是threadrun;

那LWP呢?在计算机操作系统中,LWP(Light Weight Process)即轻量级进程,是一种实现多任务的方法。(也就是两个线程的轻量级进程号,LWP也是不同的,证明我们的进程内部可以存在两个轻量级进程,那么谁是主线程呢?看LWP,LWP和PID相同的是主线程,因为我们执行的时候,还没有创建一个线程的时候,就一个线程)

那么CPU调度的时候,是看PID还是LWP呢?

其实是看LWP,因为Linix当中只有轻量级进程,调度的时候是看LWD。只不过只有一个线程的时候,LWP和PID相等。

我们先来说一个比较无关的:关于调度的时间片问题:

 当我们创建新线程的时候,在系统层面上多了一个轻量级进程,一般我们的时间片在创建的时候,时间片基本是要等分,也就是进程本来有10毫秒的时间片,实现了两线程,那么这两线程各自5毫秒,是等分的,不能说创建一个线程,就再给10毫秒,因为时间片也是共享的!所以创建线程并不影响进程切换,因为时间片是共享的。

第二个问题:我们现在创建的轻量级进程,可能会出异常,我们下面来验证一下异常:


void *threadrun(void *args)
{
    std::string name = (const char *)args;
    while (true)
    {
        sleep(1);
        std::cout << "我是新线程: name: " << name << " ,pid: " << getpid() << std::endl;
        int a = 10;
        a /= 0; // 除0错误,触发中断
    }
    return nullptr;
}

我们编译运行一下代码:

我们发现,任何一个线程崩溃,都会导致整个进程崩溃。从底层原理来讲,一个进程崩溃(除0错误,野指针错误...)系统就会转化成中断处理的方式,由操作系统为目标进程发送信号,目标进程中,信号是共享的,所有线程都会接收到。所以说多线程的健壮性是比较低的,因为一个崩掉了,进程中的全部就会崩掉,但是多进程的话,一个崩掉了并不会影响其他进程,因为进程间具有独立性。

最后一个问题:我们执行我们上面正常代码的时候,发现消息打印时混着的,这是为什么?

当多个线程各自被调度,各自每隔1秒都是往显示器上打印的,显示器的本质是文件,两个线程访问的都是同一个文件,所以两个线程向显示器打印就是向显示器文件写入,所以显示器文件本质就是一种共享资源,没有加保护的时候,我们在进行IO时会出现错误,后面我们会用锁来进行共享资源的保护。

引入pthread线程库

为什么会有一个库?这个库是什么东西?

Linux系统,不存在真正意义上的线程,它所谓的概念,就是使用轻量级进程模拟的,但是,OS中,只有轻量级进程,我们不把他叫做线程,所谓模拟线程,只是我们的说法!所以操作系统(Linux)只会为我们提供轻量级进程的系统调用。比如:

所以线程真正来说,是用轻量级进程模拟的。

Linux系统中,线程的实现实际上是通过轻量级进程(Lightweight Process,LWP)来模拟的。在Linux内核中,并没有真正意义上的线程概念,而是通过轻量级进程来实现类似线程的功能。轻量级进程是操作系统中的一种进程类型,它与传统的重量级进程(Heavyweight Process)相比,具有更低的资源开销和更快的创建、切换速度。在Linux系统中,线程的创建和管理实际上是通过轻量级进程的系统调用来实现的,这些系统调用允许程序创建多个共享同一地址空间的轻量级进程,从而模拟出线程的行为。尽管在操作系统的语境中,我们通常将这些轻量级进程称为线程,但在Linux系统内部,它们本质上仍然是轻量级进程。

其实轻量级进程封装模拟的线程,使用的是pthread库,其底层实现其实是通过clone实现的。(Windowd就有具体的线程的系统调用) 

注意:因为POSIX线程库(pthread)是操作系统提供的标准库的一部分,而不是由独立的第三方开发者或组织提供的。尽管在编译时需要显式地链接pthread库(使用-lpthread),但这并不意味着它是一个第三方库。(是系统调用的封装

所以:在C++11中,多线程编程得到了官方的支持,其在Linux系统下的实现本质上是对pthread库的封装。在Windoes下,封装了Windows对应的系统调用接口,这种封装使得C++11的多线程编程接口更加符合C++的语言特性,同时简化了线程管理、同步等操作。

在C++11中,引入了<thread>头文件,其中定义了std::thread类,用于表示和管理线程。std::thread的实现本质上是封装了pthread库的相关函数。例如:

  • 当创建一个std::thread对象并传入线程函数时,底层会调用pthread_create来创建一个线程。

  • 当调用std::threadjoin成员函数时,底层会调用pthread_join来等待线程结束。

此外,C++11还提供了其他多线程相关的功能,如std::mutex用于线程同步,std::condition_variable用于条件变量等,这些功能的底层实现也都是基于pthread库的对应功能。

#include <iostream>
#include <thread>

// 线程函数
void printHello() {
    std::cout << "Hello from thread!" << std::endl;
}

int main() {
    // 创建一个线程,执行printHello函数
    std::thread t(printHello);

    // 等待线程结束
    t.join();

    std::cout << "Hello from main!" << std::endl;

    return 0;
}

在Linux下,使用g++编译器编译时需要加上-pthread选项:

g++ -std=c++11 -pthread hello_thread.cpp -o hello_thread

所以所有后端语言多线程的底层实现,有且只有一种方案:封装 !!!

Linux线程控制的接口

在Linux系统中,创建和管理线程是一个常见的需求。本文将详细介绍如何在Linux中使用POSIX线程库(pthread)来创建和管理线程。

1. POSIX线程库

POSIX线程库(pthread)是一个广泛使用的线程库,它提供了一套API来支持多线程编程。pthread_create函数是pthread库中用于创建新线程的关键函数。

pthread_create函数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
  • thread: 返回线程ID。(是线程ID,输出型参数,线程创建成功会返回出来,我们待会儿来看看,是不是LWP呢?)

  • attr: 设置线程属性,是输入型参数,attr为NULL表示使用默认属性。

  • start_routine: 是一个函数地址,线程启动后要执行的函数。(一个函数指针,是种回调机制,代表创建出来的新线程要执行的函数入口)

  • arg: 传给线程启动函数的参数。

  • 返回值创建成功返回0,失败返回错误码。

  • 传统的一些函数是,成功返回1,失败返回-1,并且对全局变量errno赋值以指示错误。

这个库函数我们上面已经有详细说过了,这里简单回顾一下。

2. 线程等待

如果我们创建线程之后,线程就在内核当中以轻量级进程的形式运行,那么未来线程一旦创建了,主线程要对自己曾经创建的新线程进行等待,如果不等待,就会造成类似僵尸进程的问题!!!也就是内存泄漏!!! 

线程等待是必要的,因为已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。

pthread_join函数
int pthread_join(pthread_t thread, void **value_ptr);
  • value_ptr: 它指向一个指针,后者指向线程的返回值。这是一个输出型参数!

在目标线程运行结束之前,调用 join 的线程会被阻塞,无法继续执行后续代码。

现在,我们就可以来写一个偏整合代码:

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>

void showtid(pthread_t &tid)
{
    printf("tid: %ld\n", tid);
}

void *routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    int cnt = 5;
    while (cnt)
    {
        std::cout << "我是一个新线程: 我的名字是: " << name << std::endl;
        sleep(1);
        cnt--;
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, routine, (void *)"thread-1");
    (void)n;

    showtid(tid);

    pthread_join(tid, nullptr);
    return 0;
}

线程id是图中的体现吗?好像不是吧,这也太奇怪了,这也太大了吧,它竟然不是我们刚刚说的底层的LWP,其实他也就不应该是LWP,因为线程库本身就是对线程做的封装,LWP是轻量级进程的概念,是轻量级进程的id,我们要的是线程的id,既然封装了,就应该要封装得彻底,这个值有点大,我们可以将其转化为16进制:

void showtid(pthread_t &tid)
{
    printf("tid: 0x%lx\n", tid);
}

这个tid是什么鬼,暂时不说,反正我们知道它很大。

我们怎么知道我们获得的tid就是对应线程的tid呢?就是对的呢?

我们就要来学习一个POSIX接口:

phread_self函数

pthread_self 是 POSIX 线程库中的一个函数,用于获取当前线程的线程标识符tid(pthread_t 类型)。它返回调用线程的标识符,可以用于标识和管理当前线程。线程标识符被视为一个不透明的对象,常规情况下无需了解其具体值。

以下是 pthread_self 函数的详细介绍:

pthread_t pthread_self(void);

函数说明:

  1. pthread_self 函数返回当前线程的线程标识符(pthread_t 类型)。

  2. 返回的线程标识符用于在其他线程或线程管理函数中标识当前线程。

我们的代码就可以更新为:

void showtid(pthread_t &tid)
{
    printf("tid: 0x%lx\n", tid);
}

std::string FormatId(const pthread_t &tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%lx", tid);
    return id;
}

void *routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    pthread_t tid = pthread_self();
    int cnt = 5;
    while (cnt)
    {
        std::cout << "我是一个新线程: 我的名字是: " << name << " 我的Id是: " << FormatId(tid) << std::endl;
        sleep(1);
        cnt--;
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, routine, (void *)"thread-1");
    (void)n;

    showtid(tid);

    pthread_join(tid, nullptr);
    return 0;
}

我们编译运行发现:

我们果然证明了,main执行流返回的tid就是当前线程的线程id!!!所以主线程join等待的tid就是新线程。

main函数也是一个线程,也有自己的tid,我想让两个线程都跑,我们可以更新代码:

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, routine, (void *)"thread-1");
    (void)n;

    showtid(tid);
    int cnt = 5;
    while (cnt)
    {
        std::cout << "我是main线程: 我的名字是: main thread" << " 我的Id是: "
                  << FormatId(pthread_self()) << std::endl;
        sleep(1);
        cnt--;
    }

    pthread_join(tid, nullptr);
    return 0;
}

我们可以发现,不管是主线程还是新线程,都有自己的线程id。 

我现在先将结论说出来:


打印出来的 tid 是通过 pthread 库中的函数 pthread_self 得到的,它返回一个 pthread_t 类型的变量,指代的是调用 pthread_self 函数的线程的 “ID”。

怎么理解这个 “ID” 这个 “ID” 是 pthread 库给每个线程定义的进程内唯一标识,是 pthread 库维持的。由于每个进程有自己的独立内存空间,故此 “ID” 的作用域是进程级而非系统级(内核不认识)。其实 pthread 库也是通过内核提供的系统调用(例如 clone)来创建线程的,而内核会为每个线程创建系统全局唯一的 “ID” 来唯一标识这个线程。

LWP 是什么呢?LWP 得到的是真正的线程ID。之前使⽤ pthread_self 得到的这个数实际上是⼀个地址,在虚拟地址空间上的⼀个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。在 ps -aL 得到的线程ID,有⼀个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,⽽其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。⽽pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。


多线程中,代码是共享的,虽然主线程执行一部分,新线程执行另一部分,但是都可以访问公共的方法FormatId,因为地址空间是共享的,我们可以修改我们的代码,定义一个全局的flag,让新线程++,主线程打印:

所以在线程领域,全局变量或者是函数,是在线程之间是可以共享的,这是因为地址空间是共享的!

对于FormatId函数同时被两个执行流调用,也就是被重入了!

我们重入之后,使用的char id[64]缓冲区是局部,临时的,不是全局的,所以该函数被称为可重入函数! 


接下来,我们来谈谈线程传参还有返回值: 

我们线程传参其实就是一个回调,我们也就只知道这个,那么对于routine的返回值来说,该返回值是要被join接收的,而且返回值是void*类型的,什么意思呢?就好比返回值不是nullptr:

return (void*)123;//暂时表示线程退出的时候的退出码

void不占空间,但是void*要占用4/8字节,(void*)100就相当于一个地址了,是主线程将新线程创建出来的,主线程将其创建出来就是要求新线程去办事的,要给主线程拿回点东西。所以pthread_join的第二个参数void** retval,双指针是因为要传址返回,而不是传值返回,所以是void**,也就是看成是(&void*),要求是二级指针。

我们来更新一下我们的代码:

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>

int flag = 100;

void showtid(pthread_t &tid)
{
    printf("tid: 0x%lx\n", tid);
}

std::string FormatId(const pthread_t &tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%lx", tid);
    return id;
}

void *routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    pthread_t tid = pthread_self();
    int cnt = 5;
    while (cnt)
    {
        std::cout << "我是一个新线程: 我的名字是: " << name << " 我的Id是: " << FormatId(tid) << std::endl;
        sleep(1);
        cnt--;
        flag++;
    }
    return (void *)123; // 暂时表示线程退出的时候的退出码
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, routine, (void *)"thread-1");
    (void)n;

    showtid(tid);
    int cnt = 5;
    while (cnt)
    {
        std::cout << "我是main线程: 我的名字是: main thread" << " 我的Id是: " << FormatId(pthread_self()) << "flag: " << flag << std::endl;
        sleep(1);
        cnt--;
    }
    void *ret = nullptr; // ret也是一个变量,也是有空间的!!!
    // 等待的目标线程,如果异常了,整个进程都退出了,包括main线程,所以,join异常,没有意义,看也看不到!
    // jion都是基于:线程健康跑完的情况,不需要处理异常信号,异常信号,是进程要处理的话题!!!
    pthread_join(tid, &ret);                                    // 为什么在join的时候,没有见到异常相关的字段呢??
    std::cout << "ret is: " << (long long int)ret << std::endl; // 要使用的就是ret的空间,因为ret是一个指针,所以打印要转成long long int
    return 0;
}

这里有一个问题:为什么在join的时候,没有见到异常相关的字段呢??

今天我们通过rontine返回值拿到的数字是线程结束的退出码,怎么只有退出码,我们进程等待时,好歹父进程还能拿到子进程的退出信号(信号为0,表示没有收到,正常退出,信号非0,代表子进程出错时的退出码),join为什么没有拿到异常退出的信号呢?

这是因为:等待的目标线程,如果异常了,整个进程都退出了,包括main线程,所以,join异常,没有意义,看也看不到!jion都是基于:线程健康跑完的情况,不需要处理异常信号,异常信号,是进程要处理的话题!!!

还有,我们传参是可以字符串,整型,甚至是对象,返回也是对象,只要可以传对象,那么我们就可以写一个类,这个类可以是某种任务,再定义一个类,用于返回结果等等... 


因为join的第二个参数其实是输出型参数:

    void* ret=nullptr;
    pthread_join(tid, &ret);

routine的返回值就可以拿给ret,让ret带出来,但是这之间是有中转的,等我们将接口认识清楚了,我们待会儿重谈pthread库的时候再来深刻谈谈。


  • main函数结束,代表主线程结束,一般也代表进程结束,也就是说让main线程先退出,可能会影响其他线程;
  • 新线程对应的入口函数(routine),运行结束,代表当前线程运行结束;
  • 给线程传递的参数和返回值,可以是任意类型。

所以,我们就可以创建一个线程,指定一个任务,让新线程处理完后,返回结果:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstring>

// 定义一个任务类,用于封装两个整数的加法操作
class Task
{
public:
    // 构造函数,初始化两个整数
    Task(int a, int b) : _a(a), _b(b) {}
    // 执行任务,返回两个整数的和
    int Execute()
    {
        return _a + _b;
    }
    // 析构函数
    ~Task() {}

private:
    int _a; // 第一个整数
    int _b; // 第二个整数
};

// 定义一个结果类,用于封装任务的执行结果
class Result
{
public:
    // 构造函数,初始化结果
    Result(int result) : _result(result) {}
    // 获取结果
    int GetResult() { return _result; }
    // 析构函数
    ~Result() {}

private:
    int _result; // 任务的执行结果
};

// 线程入口函数
void *routine(void *args)
{
    // 将传入的参数强制转换为Task类型
    Task *t = static_cast<Task *>(args);

    // 模拟线程工作,让线程休眠1秒
    sleep(1);

    // 创建一个Result对象,存储任务的执行结果
    Result *res = new Result(t->Execute());

    // 模拟线程工作,让线程休眠1秒
    sleep(1);

    // 返回Result对象的指针
    return res;
}

int main()
{
    pthread_t tid;              // 定义线程ID
    Task *t = new Task(10, 20); // 创建一个Task对象

    // 创建线程,传入Task对象的指针作为参数
    if (pthread_create(&tid, nullptr, routine, t) != 0)
    {
        std::cerr << "Failed to create thread." << std::endl;
        delete t; // 如果线程创建失败,释放Task对象
        return -1;
    }

    // 定义一个Result指针,用于接收线程的返回值
    Result *ret = nullptr;

    // 等待线程结束,并获取线程的返回值
    if (pthread_join(tid, reinterpret_cast<void **>(&ret)) != 0)
    {
        std::cerr << "Failed to join thread." << std::endl;
        delete t; // 如果线程等待失败,释放Task对象
        return -1;
    }

    // 获取任务的执行结果并输出
    int n = ret->GetResult();
    std::cout << "新线程结束, 运行结果: " << n << std::endl;

    // 释放Task和Result对象
    delete t;
    delete ret;

    return 0;
}

 3. 线程终止

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

  1. 从线程的入口函数中进行return就是线程终止。

  2. 线程调用pthread_exit终止自己。(一定不可以使用exit,exit是进行进程终止的,不是线程终止的,除非故意而为之,调用exit会导致所有线程退出,即进程退出

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

pthread_exit函数
void pthread_exit(void *value_ptr);
  • value_ptr: 不要指向一个局部变量。(等价于return void*)

pthread_cancel函数
int pthread_cancel(pthread_t thread);
  • thread: 线程ID。(取消对应ID的线程,做法是主线程去取消新线程的,因为一般都是主线程最后退出的,除非一些特殊情况)(取消的时候,一定要保证,新线程已经启动!这样才是合法合理的)线程如果被取消,退出结果是-1【PTHREAD_CANCELED】

其实我们最多使用的还是return!!!


4. 线程分离

默认情况下,新创建的线程是joinable的,也就是线程默认是需要被等待的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

不过:

我们学习信号之后,我们发现当一个子进程在结束时,我们可以给父进程设置对子进程的SIG_IGN,父进程就可以不需要wait了,那么如果我们有个需求:让主线程不再关心新线程,当新线程结束的时候,让该新线程自己释放,不要再让主线程阻塞式的join了,我们该怎么办?

join 方法本身是阻塞的,没有直接提供非阻塞选项。我们可以设置线程为分离状态,来实现主线程不关心新线程,让主线程不等待新线程,而是想让新线程自己结束之后,自己退出,释放资源,我们就需要将线程设置为分离状态,即非joinable,或detach状态: 

pthread_detach函数
int pthread_detach(pthread_t thread);
  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。(主线程分离新线程,或者新线程把自己分离了)

但是,分离的线程,依旧在进程的地址空间中,进程的所有资源,被分离的线程,依旧可以访问,可以操作,只不过主线程不需要等待新线程!

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

void *thread_run(void * arg ) {
    pthread_detach(pthread_self());
    printf("%s\n", (char*)arg);
    return NULL;
}

int main( void ) {
    pthread_t tid;
    if ( pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0 ) {
        printf("Create thread error\n");
        return 1;
    }
    //也可以让主线程分离新线程
    pthread_detach(tid);
    int ret = 0;
    sleep(1); // 很重要,要让线程先分离,再等待
    if ( pthread_join(tid, NULL ) == 0 )
        printf("pthread wait success\n");
    else
        printf("pthread wait failed\n");
    ret = 1;
    return ret;
}

所以我们如果进行线程分离之后,就不再需要join,因为join会失败。


到目前,我们只是知道了下面的操作:(真正的原理还是不清楚的!)

  • 线程ID;
  • 线程传参和返回值;
  • 线程分离。

具体的原理我们还不清楚,我们会在下一篇谈论。


接下来,因为上面我们只是多创建一个线程,接下来,我们来创建多线程来试试看:实现一个简单的Demo:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstring>
#include <vector>

const int num = 10;

void *routine(void *args) // 是所有线程的入口函数
{
    std::string name = static_cast<const char *>(args);
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "新线程名字: " << name << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    std::vector<pthread_t> tids; // 所有的线程"ID"
    // 创建多线程
    for (int i = 0; i < num; i++)
    {
        pthread_t tid;
        // bug??
        char id[64];
        snprintf(id, sizeof(id), "thread-%d", i);
        int n = pthread_create(&tid, nullptr, routine, id);
        if (n == 0)
        {
            // 新线程创建成功
            tids.push_back(tid);
        }
        else
        {
            continue;
        }
        sleep(1); // 观察线程一个一个被创建
    }
    // 对所有的新线程进行等待
    for (int i = 0; i < num; i++)
    {
        // 要等就是要一个一个的等待:即便2号线程退出了,可是1号线程还没处理,那么2号线程就还要等
        // 不行等的话就按照分离的操作啦
        int n = pthread_join(tids[i], nullptr);
        if (n == 0)
        {
            std::cout << "等待新线程成功!" << std::endl;
        }
    }
    return 0;
}

我们打开监控脚本:

我们创建的多线程就同时跑起来了。

接下来,我们让创建出来的新线程进入自己的执行函数之后,先休眠1秒,先不着急执行,同时让创建新线程的for循环飞速执行,我们来观察一下现象:

我们观察到:被创建出来的所有的新线程的name都是thread-9,这是什么原因呢?

因为我们传递的char id[64]是属于for循环当中的临时数组,而且创建线程的时候,传递的id[64]属于该数组的起始地址,routine函数拿进来后,sleep(1)之后才更改的这个值(std::string name = static_cast<const char *>(args);),那么就有可能:新线程被创建出来,指针id[64]是拿着的,但是指针指向的数组内的内容,可能在下一次循环的时候,id被清空,因为id出一次循环,作为局部变量,重新被释放了(在回调的时候会有对args的拷贝,所以不用担心释放了就真没了),释放之后,又会写入线程2,3,4......的id值,所以指针指向没变,但是指针指向的内容一直在变化,所以当前看到的线程名就不是我们所期望的线程名。

即:

id 是一个局部变量,它在每次循环迭代时都会被重新初始化和覆盖。在 pthread_create 调用时,虽然传递的是 id 的地址作为参数,但由于 id 是局部变量,它的生命周期仅限于当前循环迭代的范围内。当线程开始执行时,它通过参数 args 访问到的 id 地址指向的内存已经被覆盖为最后一次循环迭代时的内容。换句话说,所有线程在运行时访问到的都是同一个地址,而这个地址的内容在循环结束时已经被设置为 "thread-9",因此所有线程打印的 name 都是 "thread-9"。这种行为导致了线程之间共享了同一个变量的地址,而不是每个线程拥有独立的变量内容。(多执行流访问一个公共资源,该公共资源没有加保护,这就引发了数据不一致问题

我们这里就导致了一个比较简单的线程安全的问题。

所以,我们的解决方法是不让多个线程盯着单一的资源,我们可以在堆上开辟属于一个线程的空间来使用,来保证互不干扰:(堆空间在原则上也是所有线程共享,但是只有当线程明确地访问分配给自己的那部分堆空间时,才不会受到其他线程的干扰)。这样,每个线程都有自己独立的内存区域,不会因为共享局部变量而导致数据被覆盖或混淆。

char *id = new char[64];
std::string name = static_cast<const char *>(args);
delete (char*)args;//要释放

为什么 delete[] 要在 routine 中

1. 线程参数的生命周期

main 函数中,为每个线程动态分配了一个字符串 id,并将其传递给线程函数 routine。这个字符串是动态分配的,因此需要在适当的时候释放它,以避免内存泄漏。

2. 线程的异步性

线程的执行是异步的,这意味着线程可能在 main 函数的循环结束之前或之后开始运行。如果在 main 函数中释放 id 的内存,可能会导致以下问题:

  • 如果线程在 main 函数释放内存之前开始运行,那么它访问的内存是有效的。

  • 如果线程在 main 函数释放内存之后开始运行,那么它访问的内存已经被释放,这会导致未定义行为(如访问非法内存,可能导致程序崩溃)。

为了避免这种问题,必须确保线程在访问完参数后才释放内存。因此,释放内存的逻辑应该放在线程函数 routine 中。

3. 线程函数的责任

线程函数 routine 是线程的入口点,它负责处理传递给线程的参数。因此,线程函数有责任管理它接收到的参数的生命周期。一旦线程函数完成对参数的处理,就应该释放参数占用的内存。

  • std::string name = static_cast<const char *>(args);:将 args 指向的字符串内容复制到 std::string 对象 name 中。

  • delete[] (char *)args;:释放 args 指向的动态分配的内存。这个操作必须在 routine 中进行,因为线程函数负责管理传递给它的参数。

4. 为什么 main 函数不需要释放内存

main 函数中,虽然动态分配了内存,但这些内存是传递给线程的。线程函数 routine 负责管理这些内存的生命周期。如果在 main 函数中释放这些内存,可能会导致线程访问非法内存,从而引发未定义行为。

因此,main 函数不需要释放这些内存,而是将内存管理的责任委托给线程函数 routine

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2324648.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[GXYCTF2019]禁止套娃1 [GitHack] [无参数RCE]

Git基础 Git信息泄露原理解析及利用总结 - FreeBuf网络安全行业门户 CTF中的GIT泄露_ctf git泄露-CSDN博客 Git结构 dirsearch扫出来一大堆东西&#xff08;然而这些并没有什么屁用&#xff09; 但也算起码了解了git结构了吧 /.git/HEAD&#xff1a;表示当前HEAD指针的指…

从ChatGPT到AutoGPT——AI Agent的范式迁移

一、AI Agent的范式迁移 1. ChatGPT的局限性与Agent化需求 单轮对话的“工具属性” vs. 多轮复杂任务的“自主性” ChatGPT 作为强大的生成式AI,虽然能够进行连贯对话,但本质上仍然是“工具型”AI,依赖用户提供明确的指令,而无法自主规划和执行任务。 人类介入成本过高:提…

stock-pandas,一个易用的talib的替代开源库。

原创内容第841篇&#xff0c;专注智能量化投资、个人成长与财富自由。 介绍一个ta-lib的平替——我们来实现一下&#xff0c;最高价突破布林带上轨&#xff0c;和最低价突破布林带下轨的可视化效果&#xff1a; cross_up_upper stock[high].copy()# cross_up_upper 最高价突破…

Spring Cloud Gateway详细介绍简单案例

文章目录 1、Spring Cloud Gateway 详细介绍1.1. 统一入口&#xff08;Single Entry Point&#xff09;1.2. 请求路由&#xff08;Request Routing&#xff09;1.3. 负载均衡&#xff08;Load Balancing&#xff09;1.4. 流量控制&#xff08;Rate Limiting&#xff09;1.5. 身…

鸿蒙原生开发之状态管理V2

一、ArkTS状态变量的定义&#xff1a; State&#xff1a;状态&#xff0c;指驱动UI更新的数据。用户通过触发组件的事件方法&#xff0c;改变状态数据。状态数据的改变&#xff0c;引起UI的重新渲染。 在鸿蒙原生开发中&#xff0c;使用ArkTS开发UI的时候&#xff0c;我们可以…

矩阵中对角线的遍历问题【C++】

1&#xff0c;按对角线进行矩阵排序 题目链接&#xff1a;3446. 按对角线进行矩阵排序 - 力扣&#xff08;LeetCode&#xff09; 【题目描述】 对于一个m*n的矩阵grid&#xff0c;要求对该矩阵进行 变换&#xff0c;使得变换后的矩阵满足&#xff1a; 主对角线右上的所有对角…

[Lc4_dfs] 解数独 | 单词搜索

目录 1.解数独 题解 2.单词搜索 题解 1.解数独 链接&#xff1a;37. 解数独 编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线…

day17 学习笔记

文章目录 前言一、数组的增删改查1.resize函数2.append函数3.insert函数4.delete函数5.argwhere函数6.unique函数 二、统计函数1.amax&#xff0c;amin函数2.ptp函数3.median函数4.mean函数5.average函数6.var&#xff0c;std函数 前言 通过今天的学习&#xff0c;我掌握了num…

自动语音识别(ASR)技术详解

语音识别&#xff08;Automatic Speech Recognition, ASR&#xff09;是人工智能和自然语言处理领域的重要技术&#xff0c;旨在将人类的语音信号转换为对应的文本。近年来&#xff0c;深度学习的突破推动语音识别系统从实验室走入日常生活&#xff0c;为智能助手、实时翻译、医…

git | 版本切换的相关指令

常见指令 git log --oneline #查看历史提交 git tag latest-backup # 对当前的提交进行标记&#xff0c;标记名为latest-backup git checkout -b old-version 55b16aa # 切换到[55b16aa]的提交中&#xff0c;并标记为[old-version]的分支 git checkout master …

19.OpenCV图像二值化

OpenCV图像二值化 图像二值化&#xff08;Binarization&#xff09;是图像预处理中的一种常用技术&#xff0c;其目的是将图像中的像素值分为两个类别——通常是“前景”和“背景”或者说0和255。二值化能够简化图像信息&#xff0c;为后续的形态学处理、边缘检测、目标识别等…

通过Appium理解MCP架构

MCP即Model Context Protocol&#xff08;模型上下文协议&#xff09;&#xff0c;是由Anthropic公司于2024年11月26日推出的开放标准框架&#xff0c;旨在为大型语言模型与外部数据源、工具及系统建立标准化交互协议&#xff0c;以打破AI与数据之间的连接壁垒。 MCP架构与Appi…

分享一个Pyside6实现web数据展示界面的效果图

今天又是有问题直接找DS的一天&#xff0c;每日一问&#xff0c;今天我的问题是“怎么将pyside6生成的界面转成web界面&#xff0c;使用python语言实现web界面”&#xff0c;等了一会&#xff0c;DS给我提供了两种方案&#xff0c;方案如下&#xff1a; 然后&#xff0c;让我们…

FALL靶场通关攻略

1&#xff0c;下载好靶机后打开&#xff0c;通过kali扫描靶机ip和端口&#xff0c;得到靶机ip为192.168.50.144 2&#xff0c;扫描目录 3&#xff0c;访问靶机 4&#xff0c;访问扫描到的test.php,得到缺少GET请求参数的提示 5&#xff0c;使用FUZZ来扫出参数为file 6&#xff…

Mybatis日志模块分析--适配器模式+代理模式

适配器模式 日志在我们开发过程中占据了一个非常重要的地位&#xff0c;是开发和运维管理之间的桥梁&#xff0c;在Java中的日志框架也非常多&#xff0c;Log4j,Log4j2,Apache Commons Log,java.util.logging,slf4j等&#xff0c;这些工具对外的接口也都不尽相同&#xff0c;为…

圆球法线图,图生法线图 图片生成法线图

目录 圆球法线图 根据图片生成法线图 深度图计算法线图 圆球法线图 import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D# 定义球体的参数 radius 1.0 resolution 100# 生成球体表面的点 u np.linspace(0, 2 * np.pi, resoluti…

Java基于SpringBoot的网络云端日记本系统,附源码+文档说明

博主介绍&#xff1a;✌Java老徐、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&…

【自用记录】本地关联GitHub以及遇到的问题

最近终于又想起GitHub&#xff0c;想上传代码和项目到仓库里。 由于很早之前有在本地连接过GitHub&#xff08;但没怎么用&#xff09;&#xff0c;现在需要重新搞起&#xff08;操作忘得差不多&#xff09;。 在看教程实操的过程中遇到了一些小问题&#xff0c;遂记录一下。 前…

页码设置相关问题记录

Q&#xff1a;中间没有显示页码怎么办&#xff1f; A&#xff1a;“页眉和页脚”-“页码”-“页面底端”-“普通数字2” Q&#xff1a;想让页码在某几节连续怎么办&#xff1f; A&#xff1a; ① 先保证节与节之间插入了“分节符”&#xff08;如何插入分节符和如何显示分节符…

什么是数据集市

数据集市&#xff08;Data Mart&#xff09;是数据管理领域的核心概念&#xff0c;其定义为面向特定业务领域或用户群体的小型数据仓库子集&#xff0c;专注于部门级业务分析&#xff0c;具有快速响应、灵活部署等特点。以下从定义、特点、类型、结构、应用场景及与其他数据架构…