线程概念与控制(中)

线程概念与控制(上)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 intreturn 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;              // 定义线程IDTask *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");elseprintf("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.mzph.cn/pingmian/73913.shtml

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

相关文章

【Unity】 鼠标拖动物体移动速度跟不上鼠标,会掉落

错误示范&#xff1a; 一开始把移动的代码写到update里去了&#xff0c;发现物体老是掉(总之移动非常不流畅&#xff0c;体验感很差&#xff09; void Update(){Ray ray Camera.main.ScreenPointToRay(Input.mousePosition);if (Physics.Raycast(ray, out RaycastHit hit, M…

MATLAB 控制系统设计与仿真 - 30

用极点配置设计伺服系统 方法2-反馈修正 如果我们想只用前馈校正输入&#xff0c;从而达到伺服控制的效果&#xff0c;我们需要很精确的知道系统的参数模型&#xff0c;否则系统输出仍然具有较大的静态误差。 但是如果我们在误差比较器和系统的前馈通道之间插入一个积分器&a…

VMware Windows Tools 存在认证绕过漏洞(CVE-2025-22230)

漏洞概述 博通公司&#xff08;Broadcom&#xff09;近日修复了 VMware Windows Tools 中存在的一个高危认证绕过漏洞&#xff0c;该漏洞编号为 CVE-2025-22230&#xff08;CVSS 评分为 9.8&#xff09;。VMware Windows Tools 是一套实用程序套件&#xff0c;可提升运行在 VM…

罗杰斯特回归

定义 逻辑回归其实就是原来的线性回归加了激活函数&#xff0c;这个函数其实就是sigmoid函数&#xff0c;把一个回归的连续数值压缩到了0到1的空间&#xff0c;其实只要有函数能够满足把数值压缩到0,1之间就可以&#xff08;因为0到1之间的数值就是概率值&#xff09; 对于分类…

Java多线程与JConsole实践:从线程状态到性能优化!!!

目录 一、前言二、JConsole 使用教程二、线程的基本状态2.1新建状态&#xff08;New&#xff09;2.2就绪状态&#xff08;Ready&#xff09;2.3运行状态&#xff08;Running&#xff09;2.4 阻塞状态&#xff08;Blocked&#xff09;2.5. 等待状态&#xff08;Waiting&#xff…

基于django优秀少儿图书推荐网(源码+lw+部署文档+讲解),源码可白嫖!

摘要 时代在飞速进步&#xff0c;每个行业都在努力发展现在先进技术&#xff0c;通过这些先进的技术来提高自己的水平和优势&#xff0c;图书推荐网当然不能排除在外。本次开发的优秀少儿图书推荐网是在实际应用和软件工程的开发原理之上&#xff0c;运用Python语言、爬虫技术…

《网络管理》实践环节01:OpenEuler22.03sp4安装zabbix6.2

兰生幽谷&#xff0c;不为莫服而不芳&#xff1b; 君子行义&#xff0c;不为莫知而止休。 1 环境 openEuler 22.03 LTSsp4PHP 8.0Apache 2Mysql 8.0zabbix6.2.4 表1-1 Zabbix网络规划&#xff08;用你们自己的特征网段规划&#xff09; 主机名 IP 功能 备注 zbx6svr 19…

Axure项目实战:智慧城市APP(七)我的、消息(显示与隐藏交互)

亲爱的小伙伴&#xff0c;在您浏览之前&#xff0c;烦请关注一下&#xff0c;在此深表感谢&#xff01; 课程主题&#xff1a;智慧城市APP 主要内容&#xff1a;我的、消息、活动模块页面 应用场景&#xff1a;消息页设计、我的页面设计以及活动页面设计 案例展示&#xff…

晶晨S905L3A(B)-安卓9.0-开启ADB和ROOT-支持IPTV6-支持外置游戏系统-支持多种无线芯片-支持救砖-完美通刷线刷固件包

晶晨S905L3A(B)-安卓9.0-开启ADB和ROOT-支持IPTV6-支持外置游戏系统-支持多种无线芯片-支持救砖-完美通刷线刷固件包 适用型号&#xff1a;M401A、CM311-1a、CM311-1sa、B863AV3.1-M2、B863AV3.2-M、UNT403A、UNT413A、M411A、E900V22C、E900V22D、IP112H等等晶晨S905L3A(B)处…

【免费】2007-2019年各省地方财政科学技术支出数据

2007-2019年各省地方财政科学技术支出数据 1、时间&#xff1a;2007-2019年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;行政区划代码、地区、年份、地方财政科学技术支出 4、范围&#xff1a;31省 5、指标说明&#xff1a;地方财政科学技术支出是指地方…

树形结构的工具类TreeUtil

这个地方是以null为根节点&#xff0c;相关以null或者0自己在TreeUtil中加代码&#xff0c;就行 基础类 package com.jm.common.entity;import lombok.Data;import java.util.ArrayList; import java.util.List;/*** Author:JianWu* Date: 2025/3/26 9:02*/ Data public clas…

视频联网平台智慧运维系统:智能时代的城市视觉中枢

引言&#xff1a;破解视频运维的"帕累托困境" 在智慧城市与数字化转型浪潮中&#xff0c;全球视频监控设备保有量已突破10亿台&#xff0c;日均产生的视频数据量超过10万PB。然而&#xff0c;传统运维模式正面临三重困境&#xff1a; 海量设备管理失序&#xff1a;…

DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加行拖拽排序功能示例9,TableView16_09 嵌套表格拖拽排序

前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加行拖拽排序功能示例9,TableView16_09 嵌…

QML中使用Image显示图片和使用QQuickItem显示图片

在QML中显示图片时&#xff0c;Image元素和自定义QQuickItem有不同的特性和适用场景。以下是两者的详细对比及性能分析&#xff1a; 1. Image 元素 优点&#xff1a; 声明式语法&#xff1a;简单直观&#xff0c;适合静态图片或简单动态需求 Image {source: "image.png&…

【力扣刷题|第十七天】0-1 背包 完全背包

目标和 力扣题目网址:目标和 这道题我们先用回溯的思想来做。首先我们设正数和为S&#xff0c;数组和为N&#xff0c;目标值为T&#xff0c;那么S-(N-S)T化简之后可以得S(TN)/2即选择的正数个数为偶数&#xff0c;而且NT也为偶数&#xff0c;那么第一个判断条件我们就有了&…

【Linux网络与网络编程】01.初识网络

一、计算机网络的发展历史 计算机是人的工具&#xff0c;人要协同工作&#xff0c;注定了网络的产生是必然的。 二、协议 计算机之间的传输媒介是光信号和电信号&#xff0c;通过 "频率" 和 "强弱" 来表示 0 和 1 这样的信息&#xff0c;要想传递各种不同…

使用 Python 进行链上数据监控:让区块链数据触手可及

使用 Python 进行链上数据监控:让区块链数据触手可及 区块链技术正以前所未有的速度改变着各行各业,特别是在金融、供应链、物联网和智能合约等领域的应用,已经成为了一种新常态。然而,随着区块链网络的快速扩展和去中心化特性的不断强化,数据的可视化与监控变得愈发重要…

【SMBIOS数据块类型列表】

SMBIOS数据块类型列表 SMBIOS数据块类型列表**SMBIOS 数据块类型列表****如何查看实际的 SMBIOS 数据块&#xff1f;****总结** SMBIOS数据块类型列表 在 SMBIOS&#xff08;System Management BIOS&#xff09;中&#xff0c;Type 是用来标识不同类型的数据块的。每种类型对应…

【测试】每日3道面试题 3/30

每日更新&#xff0c;建议关注收藏点赞。 白盒测试逻辑覆盖标准&#xff1f;哪种覆盖标准覆盖率最高&#xff1f; 5种。语句覆盖、分支/判定覆盖、条件覆盖、条件组合覆盖【覆盖率最高&#xff0c;所有可能条件组合都验证】、路径覆盖【理论上最高&#xff0c;但实际很难实现】…

NFS挂载异常排查记录

互相PING服务器看是否通&#xff1b;在ubuntu下看下服务器是否正常运行。导出目录是否导出了。最后发现在挂载目录的地方目录路径和后面没有加空格。