【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_exit
    pthread_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.mfbz.cn/a/361076.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Bootloader简单说明

文章目录 一、简单架构1.CAN驱动2.Flash驱动3.传输层4.诊断层5.看门狗&#xff08;Watch Dog&#xff09;6.加密算法 二、主要功能三、启动顺序与转换流程1.启动流程图2.启动顺序与转换流程说明 一、简单架构 1.CAN驱动 实现CAN报文的收发和CAN控制器硬件的操作。特点&#x…

C++20 高级编程

文章目录 前言前奏lambda浅谈std::ref的实现浅谈is_same浅谈std::function的实现std::visit 与 std::variant 与运行时多态SFINAE类型内省标签分发 (tag dispatching)编译时多态奇异递归模板模式 (Curiously Recurring Template Pattern,CRTP) 三路比较操作符 (飞船操作符) <…

蓝桥杯2024/1/28----十二届省赛题笔记

题目要求&#xff1a; 2、 竞赛板配置要求 2.1将 IAP15F2K61S2 单片机内部振荡器频率设定为 12MHz。 2.2键盘工作模式跳线 J5 配置为 KBD 键盘模式。 2.3扩展方式跳线 J13 配置为 IO 模式。 2.4 请注意 &#xff1a; 选手需严格按照以上要求配置竞赛板&#xff0c;编写和调…

C语言基础13

今天是学习嵌入式相关内容的第十四天&#xff0c;以下是今日所学内容 1.结构体: 1.结构体类型定义 2.结构体变量的定义 3.结构体元素的访问 4.结构体的存储 内存对齐 结构体整体的大小必须为最大基本类型长度的整数倍 5.结构体作为函数参数 值传递 练习:定…

数据中心IP代理是什么?有何优缺点?海外代理IP全解

海外代理IP中&#xff0c;数据中心代理IP是很热门的选择。这些代理服务器为用户分配不属于 ISP&#xff08;互联网服务提供商&#xff09;且来自第三方云服务提供商的 IP 地址&#xff0c;是分配给位于数据中心的服务器的 IP 地址&#xff0c;通常由托管和云公司拥有。 这些 I…

使用Huggingface镜像站hf-mirror.com下载资源

前言 在使用Huggingface的过程中&#xff0c;有时我们可能会遇到无法访问官方网站huggingface.co的情况&#xff0c;这可能是由于网络监管或者网络连接问题所致。然而&#xff0c;幸运的是&#xff0c;我们可以通过hf-mirror.com这个Huggingface镜像站来解决这个问题。本篇博客…

shell脚本之多行重定向 免交互 expect ssh scp; 字符处理

多行重定向 使用I/O重定向的方式将命令列表提供给交互式程序 标准输入的一种替代品 Here Document 是标准输 入的一种替代品&#xff0c;可以帮助脚本开发人员不必使用临时文件来构建输入信息&#xff0c;而是直接就地 生产出一个文件并用作命令的标准输入,Here Document 可…

TypeScript(十) Map对象、元组、联合类型、接口

1. Map对象 1.1. 简述 Map对象保存键值对&#xff0c;并且能够记住键的原始插入顺序。   任何值都可以作为一个键或一个值。 1.2. 创建 Map 使用Map类型和new 关键字来创建Map&#xff1a; 如&#xff1a; let myMap new Map([["key1", "value1"],[&…

Prometheus---图形化界面grafana(二进制)

前言 Prometheus是一个开源的监控以及报警系统。整合zabbix的功能&#xff0c;系统&#xff0c;网络&#xff0c;设备。 proetheus可以兼容网络&#xff0c;设备。容器的监控。告警系统。因为他和k8s是一个项目基金开发的产品&#xff0c;天生匹配k8s的原生系统。容器化和云原…

iOS App审核状态和审核时间管理指

引言 对于一款开发完成并准备上架的 iOS 应用程序来说&#xff0c;通过苹果公司的审核是非常重要的一步。苹果公司会对应用程序进行严格的检查&#xff0c;以确保应用程序的质量和安全性。本文将介绍 iOS 应用程序审核的流程和时间&#xff0c;希望能够帮助开发者更好地了解和…

《Is dataset condensation a silver bullet for healthcare data sharing?》

一篇数据浓缩在医疗数据集应用中的论文。 其实就是在医疗数据集上使用了data condensation的方法&#xff0c;这里使用了DM的方式&#xff0c;并且新增了浓缩时候使用不同的网络。 1. 方法 数据浓缩DC的目的是&#xff1a; E x ∼ P D [ L ( φ θ O ( x ) , y ) ] ≃ E x ∼…

CPU-Cache结构查看

参考【Ubuntu 查看 CPU 缓存】 本文主要介绍cpu的cache查看&#xff0c;以供读者能够理解该技术的定义、原理、应用。 &#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;计算机杂记 &#x1f380;CSDN主页 发狂的小花…

【昕宝爸爸小模块】深入浅出详解之常见的语法糖

深入浅出详解之常见的语法糖 一、&#x1f7e2;关于语法糖的典型解析二、&#x1f7e2;如何解语法糖&#xff1f;2.1&#x1f7e2;糖块一、switch 支持 String 与枚举2.2&#x1f4d9;糖块二、泛型2.3&#x1f4dd;糖块三、自动装箱与拆箱2.4&#x1f341;糖块四、方法变长参数…

什么是图形组态软件?可视化组态工具的特点

组态软件的定义 组态软件主要作为SCADA系统及其他控制系统的上位机人机界面的开发平台&#xff0c;为用户提供快速地构建工业自动化系统数据采集和实时监控功能服务。它使用灵活的组态方式&#xff0c;提供快速构建工业自动控制系统监控功能的通用层次的软件工具。 组态软件的…

【学网攻】 第(17)节 -- 命名ACL访问控制列表

系列文章目录 目录 前言 一、ACL(访问控制列表)是什么&#xff1f; 二、实验 1.引入 总结 文章目录 【学网攻】 第(1)节 -- 认识网络【学网攻】 第(2)节 -- 交换机认识及使用【学网攻】 第(3)节 -- 交换机配置聚合端口【学网攻】 第(4)节 -- 交换机划分Vlan【学网攻】 第…

对于this.$nextTick代码的理解

我们都知道DOM的更新是异步的,Vue的绑定原理就是用数据区驱动视图,视图也能驱动数据&#xff0c;两者是双向绑定的。 如何立马获取到更新之后的DOM呢&#xff1f; 可以使用: <template><div class"" ref"aa">{{ a }}<button click"f…

TortoiseSVN各版本汉化包下载

首先进入下载版本列表 1.下载地址&#xff1a;https://sourceforge.net/projects/tortoisesvn/files ​ 2.选择自己版本进入​ 3.选择Language Packs进入&#xff0c;选择对应语言包下载。 ​ 4.在TortoiseSVN根目录下点击安装即可。 ​

Leetcode—1265. 逆序打印不可变链表【中等】Plus

2024每日刷题&#xff08;一零三&#xff09; Leetcode—1265. 逆序打印不可变链表 实现代码 /*** // This is the ImmutableListNodes API interface.* // You should not implement it, or speculate about its implementation.* class ImmutableListNode {* public:* v…

Django模型(六)

一、其它查询 文档:https://docs.djangoproject.com/zh-hans/4.1/ref/models/querysets/#count 1.1、排序 Queryset.order_by(*fields) 默认情况下,QuerySet 返回的结果是按照模型 Meta 中的 ordering 选项给出的排序元组排序的 可以通过使用 order_by 方法在每个 QueryS…

《QDebug 2024年1月》

一、Qt Widgets 问题交流 1. 二、Qt Quick 问题交流 1.Repeator 的 delegate 在 remove 移除时的注意事项 Qt Bug Tracker&#xff1a;https://bugreports.qt.io/browse/QTBUG-47500 Repeator 在调用 remove 函数之后&#xff0c;对应的 Item 会立即释放&#xff0c;后续就…
最新文章