【Linux】多线程(线程概念+线程控制)

🌇个人主页平凡的小苏
📚学习格言:命运给你一个低的起点,是想看你精彩的翻盘,而不是让你自甘堕落,脚下的路虽然难走,但我还能走,比起向阳而生,我更想尝试逆风翻盘。
🛸C++专栏:Linux内功修炼
家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路 过的友友麻烦多多点赞关注。欢迎你们的私信提问,感谢你们的转发! 关注我,关注我,关注我,你们将会看到更多的优质内容!!

在这里插入图片描述

一、Linux线程概念

1、什么是线程

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。

  • 一切进程至少都有一个执行线程。

  • 线程在进程内部运行,本质是在进程地址空间内运行。

  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更轻量化。所以Linux下的进程称之为轻量级进程。

  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

根据我们先前的了解,一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建,虚拟地址和物理地址就是通过页表建立映射的。

image-20240129151521537

  • 每个进程都有自己独立的进程地址空间和独立的页表,也就意味着所有进程在运行时本身就具有独立性。所以我们在创建进程时,它要创建PCB,页表,建立代码和数据的映射关系…。所以创建一个进程的成本非常高。

如果我们在创建“进程”时,只创建task_struct,并要求创建出来的task_struct和父task_struct共享进程地址空间和页表,那么创建的结果就是下面这样的:

image-20240129223446968

现在创建的进程不再给你独立分配地址空间和页表,而是都指向同一块地址空间,共享同一块页表。所以这四个task_struct看到的资源都是一样的,我们后续可以通过某种方式把代码区拆分成4块,让这四个task_struct执行不同的代码区域,上述的区域(数据区,堆区,栈区)也是类似处理方式。换言之,我们后续创建的3个task_struct都各自有自己的一小份代码和数据,我们把这样的一份task_struct称之为线程

  • 其中每一个线程都是当前进程里面的一个执行流,也就是我们常说的“线程是进程内部的一个执行分支”。
  • 同时我们也可以看出,线程在进程内部运行,本质就是线程在进程地址空间内运行,也就是说曾经这个进程申请的所有资源,几乎都是被所有线程共享的。
  • 线程比进程更细,是因为其执行的代码和数据更小了
  • 线程的调度成本更低了,是因为它将来在调度的时候,核心数据结构(地址空间和页表)均不用切换了

上述谈的线程仅仅是在Linux下的实现原理,不同平台对线程的管理可能是不一样的。Linux其实并没有真正的对线程创建对应的数据结构:

  • 线程本身是在进程内部运行的,操作系统中存在大量的进程,一个进程内又存在一个或多个线程,因此线程的数量一定比进程的数量多(线程 : 进程 一定是n : 1),当线程的数量足够多的时候,很明显线程的执行粒度要比进程更细。

  • 对于这么多的线程我们OS需要对其做管理(先描述,再组织),在大部分的OS中,线程都有一个tcb。如果我们的系统实现的是真线程,比如说windows平台,它就要分别对进程和线程设计各自的描述的数据块(结构体),并且很多线程在一个进程内部,所以还要维护线程tcb和进程pcb之间的关系。所以这样写出的代码,其tcb和pcb两个数据结构之间的耦合度非常复杂。设计tcb和pcb的人认为这样的进程和线程在执行流层面上是不一样的。但是Linux不这样想:在概念上没有进程和线程的区分,只有一个叫做执行流。Linux的线程是用进程PCB模拟的。所以在Linux当中,其PCB和TCB是一回事!!!

Linux的线程用进程PCB模拟的好处很明显

  1. 不用单独设计tcb了(Linux认为tcb和pcb的属性上很大部分重叠了,不需要单独设计pcb)
  2. 不用维护tcb和pcb之间的关系了。
  3. 不用在编写任何调度算法了。
  • 答案是没有任何区别,CPU调度的时候照样以task_struct为单位来进行调度,只是这里task_struct背后的代码和页表只是曾经的代码和页表的一小部分而已。所以CPU执行的只是一小块代码和数据,但并不妨碍CPU执行其它执行流。所以我们就可以把原本串行的所有代码而转变成并发或并行的让这些代码在同一时间点得以推进。总结如下:以前CPU看到的所有的task_struct都是一个进程,现在CPU看到的所有的task_struct都是一个执行流(线程)

总览如下:

image-20240130144410542

看此图对于页表的注释,来分析下面的一份代码:

char* msg = "hello world";
*msg = 'H';

问:上述代码对吗?

  • 很明显是错的,因为字符串常量不可被修改。这时根据我们先前的学习对此做出的解释。

字符串常量区在代码区和已初始化数据区之间的,如果它不可被修改,那它是如何加载到物理内存呢?或者说是谁保证它不可被修改的?

  • 根本原因就是当你尝试进行修改时,页表有对应的条目限制你的更改。比如说我字符串常量区经过页表的映射到物理内存,当它从虚拟地址到物理地址转换的时候,它是只读的,所以RWX权限为R,所以尝试在修改的时候直接在页表进行拦截,并结合mmu内存管理单元,识别到只读但尝试修改的异常,发出信号,随后OS把此进程直接干掉。

问:有了线程的引入,该如何重新理解之前的进程?

曾经我们理解的进程 = 内核数据结构 + 进程对应的代码和数据,现在的进程,站在内核角度上看就是:承担分配系统资源的基本实体(进程的基座属性)。所有进程最大的意义是向系统申请资源的基本单位。

  • 因此,所谓的进程并不是通过task_struct来衡量的,除了task_struct之外,一个进程还要有进程地址空间、文件、信号等等,合起来称之为一个进程。换言之,当我们创建进程时是创建一个task_struct、创建地址空间、维护页表,然后在物理内存当中开辟空间、构建映射,打开进程默认打开的相关文件、注册信号对应的处理方案等等。

我们之前接触到的进程内部都只有一个task_struct,也就是该进程内部只有一个执行流,即单执行流进程

而内部可以有多个执行流的进程我们称之为多执行流进程

  • 所以Linux下没有真正意义上的线程,而是用进程task_struct模拟实现的。所以CPU看到的实际上的task_struct实体是要比传统意义上的进程更轻量化的。所以Linux下的“进程” <= 其它操作系统的进程概念。
  • 线程就是调度的基本单位

2、二级页表

我们以32位平台为例,在32位平台下一共有232个地址,地址空间的单位就是232 * 1字节 = 4GB。此时如果做地址之间的映射,每个虚拟地址都要有对应的物理地址。如果页表只有一张,那么需要多少条目(页表项)呢?答案是232个条目,即这张表一共有232个映射表项。

image-20240130172358982

每一个表项中除了要有虚拟地址和与其映射的物理地址以外,实际还需要有一些权限相关的信息,比如我们所说的用户级页表和内核级页表,实际就是通过权限进行区分的。

image-20240130172415726

注意

  • 每一个条目可不是只有1个字节,保守估计有8个字节,那么保存一张页表需要维护2^32 * 8字节 = 32GB。现在光页表都32GB这么大了,我物理内存才多大,一张页表干下去我内存还剩什么呢?

所以我们实际的页表并不是这样子的,我们的页表是多级页表,在32位平台下是二级页表。

image-20240130172438600

我们的cpu通过地址空间访问物理内存的时,cpu读取指定的数据和代码然后根据指定的地址返回物理内存的时候,cpu出来的地址是虚拟地址,我们的进程地址空间是2^32个,我们的虚拟地址是32位。而虚拟地址在被转化的过程中,不是直接转化的!而是拆分成了10 + 10 + 12!

32位平台下,虚拟地址映射转化的过程如下:

  • 选择虚拟地址的前10个比特位在页目录当中进行查找,找到对应的页表。

  • 再选择虚拟地址的10个比特位在对应的页表当中进行查找,找到物理内存中对应页框的起始地址。

  • 最后将虚拟地址中剩下的12个比特位作为偏移量从对应页框的起始地址处向后进行偏移,找到物理内存中某一个对应的字节数据。

物理内存在划分的时候是按4KB位单位进行划分的(这里的4KB叫做页框),可执行程序按照虚拟地址空间编译,也划分号了4KB(这里的4KB叫做页帧)。我们的文件系统在和物理内存进行IO的时候,其基本单位是块,一般是4KB。

  • 我们假设物理内存是4GB,大概有4 * 1024 * 1024KB / 4KB = 220个页,大约100万个页。页框也就是有220个,那么OS就要管理他们(先描述,再组织)。因此OS内部用一个struct page这样的数据结构来进行描述,通过struct page mem[1024*1024]来组织。此时对内存的管理,就变成了对数组的增删查改。

虚拟地址映射过程图示如下:

image-20240130172543173

如果页表只有1张,要占2^32 / 2^12 = 2^20条目,即使一个条目10字节,页表最大也就10M到20M。如果把整个页旋转一下,把页目录放上面,就相当于一颗多叉树。

  • 上面所说的所有映射过程,都是由MMU(MemoryManagementUnit)这个硬件完成的,该硬件是集成在CPU内的。页表是一种软件映射,MMU是一种硬件映射,所以计算机进行虚拟地址到物理地址的转化采用的是软硬件结合的方式。

总结上述页表这样设计的好处

  • 进程虚拟地址管理和内存管理,通过页表 + page进行了解耦
  • 页表分离了,可以实现页表的按需获取,没有用到的就不创建
  • 分页机制 + 按需创建页表 = 节省空间

3、线程优点

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

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

  • 线程占用的资源要比进程少很多

  • 能充分利用多处理器的可并行数量

  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

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

注意:

  • 计算密集型:执行流的大部分任务,主要以计算为主。比如加密解密、大数据查找等。
  • IO密集型:执行流的大部分任务,主要以IO为主。比如刷磁盘、访问数据库、访问网络等。

4、线程缺点

  • 性能损失: 一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低: 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说,线程之间是缺乏保护的。
  • 缺乏访问控制: 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高: 编写与调试一个多线程程序比单线程程序困难得多。

5、线程异常

  • 单个线程如果出现除零、野指针等问题导致线程崩溃,进程也会随着崩溃。
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

6、线程用途

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

二、Linux进程VS线程

1、进程和线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位

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

  1. 线程ID
  2. 一组寄存器
  3. 有独立的栈结构
  4. errno
  5. 信号屏蔽字
  6. 调度优先级

2、进程的多个线程共享

因为是在同一个地址空间,因此所谓的代码段(Text Segment)、数据段(Data Segment)都是共享的:

  • 如果定义一个函数,在各线程中都可以调用。

  • 如果定义一个全局变量,在各线程中都可以访问到。

除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表。(进程打开一个文件后,其他线程也能够看到)
  • 每种信号的处理方式。(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
  • 当前工作目录。(cwd)
  • 用户ID和组ID。

三、线程控制

1、POSIX线程库

原生线程库pthread

  • 在Linux中,站在内核角度没有真正意义上线程相关的接口,但是站在用户角度,当用户想创建一个线程时更期望使用thread_create这样类似的接口,而不是vfork函数,因此系统为用户层提供了原生线程库pthread。

  • 原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口。

  • 因此对于我们来讲,在Linux下学习线程实际上就是学习在用户层模拟实现的这一套接口,而并非操作系统的接口。

pthread线程库是应用层的原生线程库

  • 应用层指的是这个线程库并不是系统接口直接提供的,而是由第三方帮我们提供的。
  • 原生指的是大部分Linux系统都会默认带上该线程库。
  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
  • 要使用这些函数库,要通过引入头文件<pthreaad.h>。
  • 链接这些线程函数库时,要使用编译器命令的“-lpthread”选项。

错误检查

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

  • pthreads函数出错时不会设置全局变量errno(而大部分POSIX函数会这样做),而是将错误代码通过返回值返回。

  • pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小。

2、线程创建pthread_create

创建线程的函数叫做pthread_create,其函数原型如下:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数说明

  • thread:获取创建成功的线程ID,该参数是一个输出型参数。
  • attr:用于设置创建线程的属性,传入NULL表示使用默认属性。
  • start_routine:返回值和参数均为void*的函数指针。该参数表示线程例程,即线程启动后要执行的函数。
  • arg:传给线程例程的参数。

返回值说明

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

注意

  • Linux不能真正意义上的帮我们提供线程的接口,但是Linux有原生线程库,使用此函数必须在编译时带上 -pthread 选项。

3、获取线程ID pthread_self

常见获取线程ID的方式有两种:

  1. 创建线程时通过输出型参数获得。
  2. 通过调用pthread_self函数获得。

pthread_self函数的函数原型如下:

pthread_t pthread_self(void);

4、线程等待pthread_join

首先需要明确的是,一个线程被创建出来,这个线程就如同进程一般,也是需要被等待的。如果主线程不对新线程进行等待,那么这个新线程的资源也是不会被回收的。所以线程需要被等待,如果不等待会产生类似于“僵尸进程”的问题,也就是内存泄漏。等待线程的函数叫做pthread_join,函数原型如下:

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

参数说明

thread:被等待线程的ID。
retval:线程退出时的退出码信息。
返回值说明

线程等待成功返回0,失败返回错误码。

5、线程终止

如果需要只终止某个线程而不是终止整个进程,可以有三种方法:

  1. 从线程函数return。
  2. 线程可以自己调用pthread_exit函数终止自己。
  3. 一个线程可以调用pthread_cancel函数终止同一进程中的另一个线程

方法一从线程函数return

  • 此法我们在上面已经见过,就不做演示。

方法二(pthread_exit

  • pthread_exit函数的功能就是终止线程,pthread_exit函数的函数原型如下:
#include <pthread.h>
void pthread_exit(void *retval);

参数说明

  • retval:线程退出时的退出码信息。

注意

  • 该函数无返回值,跟进程一样,线程结束的时候无法返回它的调用者(自身)。
  • pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。

例如,在下面代码中,我们使用pthread_exit函数终止线程,并将线程的退出码设置为1111:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
static void printTid(const char *name, const pthread_t &tid)
{printf("%s 正在运行, thread id: 0x%x\n", name, tid);
}
void *startRoutine(void *args)
{const char *name = static_cast<const char *>(args);int cnt = 5;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--)){break;}}cout << "线程退出啦...." << endl;//1、线程退出方式1: 从线程函数直接return/*return (void *)111;*///2、线程退出方式2: pthread_exitpthread_exit((void*)1111);
}
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1");(void)n;void *ret = nullptr;pthread_join(tid, &ret);cout << "main thread join success, *ret: " << (long long)ret << endl;sleep(10);while (true){printTid("main thread", pthread_self());sleep(1);}return 0;
} 

image-20240131112834783

这段代码我们也能看出使用pthread_exit只能退出当前子线程,不会影响其它线程。

问:为何终止线程要用pthread_exit, exit 不行吗?

看如下的代码:

#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
using namespace std;
__thread int global_value = 100;
void *startRoutine(void *args)
{while (true){cout << "thread" << pthread_self() << " global_value: " << global_value<< " Inc: " << global_value++ << "lwp: " << syscall(SYS_gettid) << endl;sleep(1);break;}exit(1);
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");int n = pthread_join(tid1, nullptr);cout << n << ":" << strerror(n) << endl;n = pthread_join(tid2, nullptr);cout << n << ":" << strerror(n) << endl;n = pthread_join(tid3, nullptr);cout << n << ":" << strerror(n) << endl;return 0;
}

image-20240131182722823

总结:

  • exit是退出进程,任何一个线程调用exit,都表示整个进程退出。无论哪个子线程调用整个程序都将结束。 而pthread_exit的作用是只退出当前子线程,记住是只。即使你放在主线程,它也会只退出主线程,其它线程有运行的仍会继续运行。

方法三:(pthread_cancel)

  • 线程是可以被取消的,我们可以使用pthread_cancel函数取消某一个线程,pthread_cancel函数的函数原型如下:
#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数说明

  • thread:被取消线程的ID。

返回值说明

  • 线程取消成功返回0,失败返回错误码。

线程是可以取消自己的,取消成功的线程的退出码一般是-1。例如在下面的代码中,我们让线程执行一次打印操作后将自己取消:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
static void printTid(const char *name, const pthread_t &tid)
{printf("%s 正在运行, thread id: 0x%x\n", name, tid);
}
void *startRoutine(void *args)
{const char *name = static_cast<const char *>(args);int cnt = 5;while (true){printTid(name, pthread_self());sleep(1);if (!(cnt--)){// break;}}
}
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1");(void)n;sleep(3);//代表main thread对应的工作cout << "new thread been canceled" << endl;pthread_cancel(tid);void *ret = nullptr;pthread_join(tid, &ret);cout << "main thread join success, *ret: " << (long long)ret << endl;sleep(10);while (true){printTid("main thread", pthread_self());sleep(1);}return 0;
}

image-20240131182959891

为什么退出的结果是-1呢?

  • 线程和进程一样,用的都是PCB,退出时都有自己的退出码,调用return或exit就是自己修改PCB中的退出结果(退出码),取消这个线程时,是OS取消的,就直接向退出码中写-1。
  • 这里的-1就是pthread库里头给我们提供的宏(PTHREAD_CANCELED)

上述我们做的测试是让main thread主线程去取消新线程new thread,不推荐反过来。这里就不做测试了。

6、线程栈 && pthread_t

pthread_t实际上就是地址。

  • 线程是一个独立的执行流
  • 线程一定会在自己的运行过程中,产生临时数据(调用函数,定义局部变量等)
  • 线程一定需要有自己的独立的栈结构

线程的独立栈结构

我们使用的线程库,是用户级线程库:pthread。是因为Linux没有真线程,没有办法提供真的线程调用接口,只能提供创建子进程、共享地址空间的调用接口。但是进程的代码、数据……怎么划分这些都是由线程库自己维护的。注意:此pthread库是动态库。

  • 因为要把此动态库加载到物理内存,所以我的磁盘中有如上(libpthread.so动态库 & mypthread.exe可执行程序)。我们在运行时,首先要把此可执行程序mypthread.exe加载到内存,此程序内部的代码中一定有pthread_create,pthread_join这些从libpthread.so动态库里调来的函数,所以此时OS把该动态库加载到内存。随后把此动态库经过页表映射到进程地址空间的共享区当中,我们的task_truct通过虚拟地址访问代码区然后跳转至共享区内,执行相关的创建线程等工作,执行后再返回至代码区。
  • 所以最终都是在地址空间中的共享区内完成对应的线程创建等操作的。
  • 所以在我们的代码中一定充斥着三大部分(你的,库的,系统的)。所有的代码都是在进程的地址空间当中进行执行的。

问:pthread_t究竟是什么?

既然我们已经知道此动态库会被加载到共享区,那么我们把此共享区的libpthread.so动态库放大来讨论。线程的全部实现,并没有全部体现在OS内,而是OS提供执行流,具体的线程结构由库来进行管理。如下:

  • 操作系统只提供轻量级进程,对于用户他不管,只要线程。所以在用户和OS之间设计了libpthread.so库,用于创建线程,等待线程……操作。用户创建一个线程,库做了转换,让你在系统帮你创建一个轻量级进程,用户终止一个线程,库帮你终止一个轻量级进程,用户等待一个线程,库帮你转换成等待一个轻量级进程,并且把结果返回。此库起到的就是承上启下的作用。

image-20240131193315156

库可以创建多个线程,需要对这些线程进行管理(先描述,再组织)。库里头通过类似struct thread_info的结构体(注意里头是有私有栈的)来进行管理:

struct thread_info
{pthread_t tid;void *stack; // 私有栈...
}

当你在用户层每创建一个线程时,在库里头就会创建一个线程控制块struct thread_info(描述线程的属性)。给创建线程的用户返回的是该结构体的起始虚拟地址。所以我们的pthread_t实际上就是用户级线程的控制结构体的起始地址!!!。

image-20240131193454373

既然每一个线程都有struct thread_info结构体,而此结构体内部又有私有栈,所以结论如下:

  • 主线程的独立栈结构,用的就是地址空间中的栈区
  • 新线程用的栈结构,用的是库中提供的栈结构

7、线性的局部存储

我们的线程除了保存临时数据时可以有自己的线程栈,我们的pthread给我们了一种能力,如果定义了一个全局变量(默认所有线程共享),但是你想让每个线程各自私有,那么我们就可以使用线程局部存储。

如下我们创建了3个线程,创建一个全局变量,默认情况下此全局变量所有线程共享,现在我们来打印此全局变量以及地址来观察现象:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int global_value = 100;
void *startRoutine(void *args)
{while (true){cout << "thread" << pthread_self() << " global_value: " << global_value<< " &global_value: " << &global_value << " Inc: " << global_value++ << endl;sleep(1);}
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}

正常情况下,我们观察到着三个线程打印的全局变量地址应该都是一样的,且打印的变量是在累加的,这是正常的,因为共享全局变量,我的修改别人也能拿到。

image-20240131193708304

为了让此全局变量独属于各个线程所私有,我们只需要给全局变量前假设__thread即可,加了这个__thread就会默认把这个global_value再拷一份给每一个进程。

__thread int global_value = 100;

代码如下:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
__thread int global_value = 100;
void *startRoutine(void *args)
{while (true){cout << "thread" << pthread_self() << " global_value: " << global_value<< " &global_value: " << &global_value << " Inc: " << global_value++ << endl;sleep(1);}
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}

如下可以看到,创建的3个线程,每个线程的全局变量的地址都是不一样的,修改变量时,互相之间没有影响,各自独立。

image-20240131193813750

8、分离线程pthread_detach

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏。
  • 但如果我们不关心线程的返回值,join也是一种负担,此时我们可以将该线程进行分离,后续当线程退出时就会自动释放线程资源。
  • 一个线程如果被分离了,这个线程依旧要使用该进程的资源,依旧在该进程内运行,甚至这个线程崩溃了一定会影响其他线程,只不过这个线程退出时不再需要主线程去join了,当这个线程退出时系统会自动回收该线程所对应的资源。
  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

分离线程的函数叫做pthread_detach,pthread_detach函数的函数原型如下:

#include <pthread.h>
int pthread_detach(pthread_t thread);

参数说明:

  • thread:被分离线程的ID。

返回值说明:

  • 线程分离成功返回0,失败返回错误码。

joinable和分离是冲突的,一个线程不能既是joinable又是分离的。我们编写如下的代码进行验证:

#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
using namespace std;
__thread int global_value = 100;
void *startRoutine(void *args)
{pthread_detach(pthread_self());cout << "线程分离..." << endl;while (true){cout << "thread" << pthread_self() << " global_value: " << global_value<< " Inc: " << global_value++ << "lwp: " << syscall(SYS_gettid) << endl;sleep(1);}
}
int main()
{pthread_t tid1;pthread_t tid2;pthread_t tid3;pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 1");pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 1");int n = pthread_join(tid1, nullptr);cout << n << ":" << strerror(n) << endl;n = pthread_join(tid2, nullptr);cout << n << ":" << strerror(n) << endl;n = pthread_join(tid3, nullptr);cout << n << ":" << strerror(n) << endl;return 0;
}

image-20240131205434136

不是说好一个线程不能既是joinable又是分离的吗,下面我们对上述代码进行一次小改动,仅仅多了一个sleep(1):

image-20240131205452342

image-20240131205500183

为什么我sleep(1)后才符合我们的预期呢?( 一个线程不能既是joinable又是分离的)。有sleep之后join就会失败,没有sleep,join就会成功,那么哪个才是正确的呢?

  • 有sleep(1)才是正确的。原因是当我们床架线程后,新线程就跑去执行我的线程处理函数了,而主线程继续向后执行,新线程和主线程本质都是轻量级进程,谁先被调度这个是不确定的,那么就很有可能创建新线程后,主线程直接进入join等待(没有sleep(1)),而新线程还没来得及进行线程分离pthread_detach,主线程join后就被挂起了,阻塞了,当你再去分离的时候,已经没有时间join了,也不会唤醒你了。
  • 而加上sleep(1)后就是为了让新线程先去detach后再去分离

总结分离线程:

  1. 线程分离了,意味着,不在关心这个线程的死活。所以这也相当于线程退出的第4种方式,延后退出。
  2. 立即分离或者延后分离都可以,但是要保证线程活着。
  3. 新线程分离,但是主线程先退出(进程退出),所有线程就都退了。
  4. 一般分离线程,对应的主线程不退出(常驻内存的进程)

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

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

相关文章

负载均衡下的webshell上传+nginx解析漏洞

负载均衡下的webshell上传 一&#xff0c;负载均衡下webshell上传的四大难点 难点一&#xff1a;需要在每一台节点的相同位置上传相同内容的webshell 我们需要在每一台节点的相同位置都上传相同内容的 WebShell一旦有一台机器上没有&#xff0c;那么在请求轮到这台机器上的时…

二分查找-迭代法

Go 算法 每天5道&#xff0c;开心快乐每一天 一点都不开心 哈哈哈哈哈哈 -2.1 day 1 1.22&#xff08;1.23 1.25 1.29&#xff09; 1.23 已复习 704. 二分查找 力扣题目链接 //左闭右开 func search(nums []int, target int) int { right : len(nums) left : 0; for le…

React18-模拟列表数据实现基础表格功能

文章目录 分页功能分页组件有两种接口参数分页类型用户列表参数类型 模拟列表数据分页触发方式实现目录 分页功能 分页组件有两种 table组件自带分页 <TableborderedrowKey"userId"rowSelection{{ type: checkbox }}pagination{{position: [bottomRight],pageSi…

海外多语言盲盒App开发:机遇与挑战并存

随着全球化进程的加速&#xff0c;跨文化交流成为人们日常生活和工作中的重要部分。在此背景下&#xff0c;多语言盲盒App的开发成为了一个具有巨大潜力的市场。本文将探讨海外多语言盲盒App开发的机遇与挑战&#xff0c;以及如何应对这些挑战。 一、海外多语言盲盒App开发的机…

AI赋能编程 | 自动化工具助力高效办公

前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家&#xff1a;https://www.captainbed.cn/z ChatGPT体验地址 文章目录 前言泡泡AI工具卡片思维导图Markdown编辑器 其他工具文件免费处理工具结语 合集…

C++引用详解

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C/C》 《LeedCode刷题》 键盘敲烂&#xff0c;年薪百万&#xff01; 一、引用的概念 引用不是新定义一个变量&#xff0c;而是给已存在变量取了一个别名&#xff0c;编译器不会为引用变量开辟内存空间…

使用 Paimon + StarRocks 极速批流一体湖仓分析

摘要&#xff1a;本文整理自阿里云智能高级开发工程师王日宇&#xff0c;在 Flink Forward Asia 2023 流式湖仓&#xff08;二&#xff09;专场的分享。本篇内容主要分为以下四部分&#xff1a; StarRocksPaimon 湖仓分析的发展历程使用 StarRocksPaimon 进行湖仓分析主要场景和…

RPC教程 7.服务发现与注册中心

0.前言 这一节的内容只能解决只有一个服务的情况。要是有多个服务(即是多个结构体&#xff09;这种就解决不了&#xff0c;也即是没有服务ip地址和服务实例的映射关系。 1.为什么需要注册中心 在上一节中&#xff0c;客户端想要找到服务实例的ip,需要硬编码把ip写到代码中。…

如何将Mac连接到以太网?这里有详细步骤

在Wi-Fi成为最流行、最简单的互联网连接方式之前&#xff0c;每台Mac和电脑都使用以太网电缆连接。这是Mac可用端口的标准功能。 如何将Mac连接到以太网 如果你的Mac有以太网端口&#xff0c;则需要以太网电缆&#xff1a; 1、将电缆一端接入互联网端口&#xff08;可以在墙…

IDEA2023打开新项目默认SDK变成了17

问题描述 项目安装了2个sdk版本&#xff0c;jdk8和jdk17 自从升级IDEA版本到2023以后&#xff0c;每次打开新项目&#xff0c;sdk都被默认选择成了jdk17, 每次都得手动修改 &#xff08;File--Project Structure&#xff09;&#xff0c;超级麻烦。 没有用的解决方法 以下这…

正则表达式与文本三剑客

目录 一、正则表达式 1. 定义 2. 字符匹配 3. 重复限定符 4. 位置锚点 5. 分组和引用 6. 扩展正则表达式 二、文本三剑客 1. grep 1.1 定义 1.2 语法 1.3 选项 1.4 示例 2. sed 2.1 定义 2.2 通式 2.3 选项 2.4 脚本格式&#xff08;脚本语法&#xff09; 2.…

Backtrader 文档学习- Broker - Cheat-On-Open

Backtrader 文档学习- Broker - Cheat-On-Open 1.概述 V1.9.44.116增加了Cheat On Open的支持。对于全押的人来说&#xff0c;这似乎是一个必需的功能&#xff0c;用bar的收盘价后进行计算&#xff0c;希望与开盘价相匹配。 当开盘价差距&#xff08;上涨或下跌&#xff0c;取…

DjangoURL调度器(二)

一、默认值与额外参数 1.1、默认值 1.1.1、urls.py from django.urls import pathfrom . import viewsurlpatterns [# http://127.0.0.1:8000/polls/blog/ 等同于 # http://127.0.0.1:8000/polls/blog/1/path(blog/, views.page),# http://127.0.0.1:8000/polls/blo…

BSV区块链将凭借Teranode的创新在2024年大放异彩

​​发表时间&#xff1a;2024年1月15日 2024年1月15日&#xff0c;瑞士楚格 – BSV区块链协会研发团队今日官宣了Teranode的突破性功能&#xff0c;这些功能将显著提升BSV区块链网络的效率和速度。在不久的将来&#xff0c;BSV区块链的交易处理能力将达到每秒100万笔交易。 T…

怎么备份ESXi虚拟机?

ESXI备份虚拟机与快照的区别是什么&#xff1f; 关于虚拟机备份的一个常见误解是它与虚拟机快照有何不同以及它们是否可以相互替代&#xff1f;为了回答这个问题&#xff0c;让我们仔细看看这两个概念&#xff1a; VM虚拟机快照&#xff1a;快照&#xff08;或Hyper-V 的检查…

神经网络的一些常规概念

epoch&#xff1a;是指所有样本数据在神经网络训练一次&#xff08;单次epoch(全部训练样本/batchsize)/iteration1&#xff09;或者&#xff08;1个epochiteration数 batchsize数&#xff09; batch-size&#xff1a;顾名思义就是批次大小&#xff0c;也就是一次训练选取的样…

网络安全03---Nginx 解析漏洞复现

目录 一、准备环境 二、实验开始 2.1上传压缩包并解压 2.2进入目录&#xff0c;开始制作镜像 2.3可能会受之前环境影响&#xff0c;删除即可 ​编辑 2.4制作成功结果 2.5我们的环境一个nginx一个php 2.6访问漏洞 2.7漏洞触发结果 2.8上传代码不存在漏洞 2.9补充&#…

Log4j2-29-log4j2 discard policy 极端情况下的丢弃策略 同步+异步配置的例子

Log4j2异步日志、同步日志和混合日志的配置详解 Log4j 2中记录日志的方式有同步日志和异步日志两种方式&#xff0c;其中异步日志又可分为使用AsyncAppender和使用AsyncLogger两种方式。 异步日志(性能最好&#xff0c;推荐使用) 异步日志情况下&#xff0c;增加 Disruptor …

深入浅出HBase:一文理解HBase基础概念(列存储、时间戳、key-value)、架构特点以及适合的使用场景

文章目录 一. HBase 数据模型1. 行存储与列式存储1.1. 行存储1.2. 列存储 2. HBase 数据模型2.1. 模型概览2.2. 列与列族2.3. 时间戳&#xff1a;定义数据版本2.4. HBase的Key-Value 三. HBase架构1. HBase读写流程简述2. HRegionServer内部内部数据流转&#xff1a;HRegion &l…

Web性能优化之如何评估网页性能——性能指标和度量工具介绍

前言 用户在访问 web 网页时&#xff0c;大部分都希望网页能够在一秒完成。事实上&#xff0c;加载时间每多 1 秒&#xff0c;就会流失 7%的用户。如果时间超过 8s 用户就会感到不耐烦、会放弃访问。这也就是著名的 “8秒原则”。 虽然当今设备及网络环境都大幅提升&#xff…