【Linux】多线程——线程概念|进程VS线程|线程控制

> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:理解【Linux】多线程——线程概念|进程VS线程|线程控制

> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

> 专栏选自:Linux初阶

> 望小伙伴们点赞👍收藏✨加关注哟💕💕

​​

🌟前言

早期我们的计算机还只能单个进程运行,这样的话每个进程就只能独立存在,不可以进行每个进程交互,在这个基础上我们的先人(大佬)看看能不能多个进程同时运行,也就有了现在的多进程,那对比单个进程,多进程有什么优势呢?值不值得我们学习呢?咱们带上这两个问题来康康Linux下的多线程。

⭐主体

学习【Linux】多线程——线程概念|进程VS线程|线程控制咱们按照下面的图解:

​🌙 地址空间和页表

地址空间是进程能看到的资源窗口:一个进程能看到代码区、共享区、内核区、堆栈区,大部分的资源都是在地址空间上看到的。

页表决定进程真正拥有资源的情况:当前进程认为自己有了4GB,可是实际上用了多少由页表决定最终能用多少物理资源。

合理的对地址空间与页表进行资源划分,我们就可以对一个进程所有的资源进行分类:通过地址空间分为栈区、堆区…通过页表映射到不同的物理内存。

💫 页表的映射

在32位平台下一共有2³²个地址,也就意味着有2³²个地址需要被映射:

地址空间一共有2³²个地址,每个地址单位都是1字节,而页表也得有2³²个条目,每个地址都得经过页表映射,都是页表的每个条目(包括物理地址,包括是否命中,包括RWX权限,包括U/K权限,一个条目,假设为6个字节,样例数据),所以,光保存页表所需空间为24GB(4GB约为40亿字节)。

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

每个应表项中存储一个物理地址和一个虚拟地址就需要8个字节,考虑到还需要包含权限相关的各种信息,这里每一个表项就按10个字节计算:

  • 这里一共有2³²个表项,也就意味着存储这张页表需要用2³² * 10个字节,也就是40GB
  • 而在32位平台下我们的内存可能一共就只有4GB,也就是说我们根本无法存储这样的一张页表

💫 二级页表

  • 虚拟地址在被转化的过程中,不是直接转化的!而是拆分成了10 + 10 + 12
  • 以32位平台为例,其页表的映射过程如下:
  1.  选择虚拟地址的前10个比特位在页目录当中进行查找,找到对应的页表。
  2. 再选择虚拟地址的10个比特位在对应的页表当中进行查找,找到物理内存中对应页框的起始地址。
  3. 最后将虚拟地址中剩下的12个比特位作为偏移量从对应页框的起始地址处向后进行偏移,找到物理内存中某一个对应的字节数据。 
  • 相关说明:
  1.  物理内存实际是被划分成一个个4KB大小的页框的,而磁盘上的程序也是被划分成一个个4KB大小的页帧的,当内存和磁盘进行数据交换时也是以4KB大小为单位进行加载和保存的。
  2. 4KB = 2¹²个字节,一个页框中有2¹²个字节,而访问内存的基本大小是1字节,因此一个页框中就有2¹²个地址,于是就可以将剩下的12个比特位作为偏移量,从页框的起始地址处开始向后进行偏移,从而找到物理内存中某一个对应字节数据。 
  • 这实际上就是所谓的二级页表,其中页目录项是一级页表,页表项是二级页表
  1.  每一个表项还是按10字节计算,页目录和页表的表项都是2¹º个,因此一个表的大小就是2¹º也就是10个字节,也就是10KB
  2. 页目录有2¹º个表项也就意味着页表有2¹º个,也就是说一级页表有1张,二级页表有2¹º张,总共算下来大概就是10MB,内存消耗并不高,因此Linux中实际就是这样映射的。 
  • **注意:**Linux中,32位平台用的是二级页表,64位平台用的是多级页表

  

​🌙 进程基础概念


💫 线程是什么

概念:

在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列 ”一切进程至少都有一个执行线程;线程在进程内部运行,本质是在进程地址空间内运行在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流不同平台的多线程底层实现策略不一样,我们讨论Linux平台。

进程对应的模型:进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建,虚拟地址和物理地址就是通过页表建立映射的:

进程=内核数据结构+代码和数据,每个进程都有自己独立的进程地址空间和独立的页表,也就意味着所有进程在运行时本身就具有独立性,我们在创建“进程”时,只创建PCB,并要求创建出来的PCB不在独立创建,与父进程同享PCB,那么创建的结果就是下面这样的

因为我们可以通过虚拟地址空间+页表的方式对进程进行资源划分,单个“进程”执行力度一定要比之前的进程要细。上图中每个线程都是当前进程里的一个执行流,线程在进程内部运行,线程在进程的地址空间内运行,拥有该进程的一部分资源。

💫 如何理解线程

  • 每个进程都有自己独立的进程地址空间和独立的页表,也就意味着所有进程在运行时本身就具有独立性。
  1. 在创建进程时,它要创建PCB,页表,建立代码和数据的映射关系…
  2. 所以创建一个进程的成本非常高。
  • 如果创建"进程"时,只创建task_struct,并要求创建出来的task_struct和父task_struct共享进程地址空间和页表。
  1. 现在创建的进程不再给你独立分配地址空间和页表,而是都指向同一块地址空间,共享同一块页表。
  2. 所以这四个task_struct看到的资源都是一样的,后续可以通过某种方式把代码区拆分成4块,让这四个task_struct执行不同的代码区域。
  3. 上述的区域(数据区,堆区,栈区)也是类似处理方式。
  4. 换言之,后续创建的3个task_struct都各自有自己的一小份代码和数据,把这样的一份task_struct称之为线程。
  5. 其中每一个线程都是当前进程里面的一个执行流,也就是常说的"线程是进程内部的一个执行分支"。

  • 线程在进程内部运行,本质就是线程在进程地址空间内运行,也就是说曾经这个进程申请的所有资源,几乎都是被所有线程共享的。

  1. 线程比进程更细,是因为其执行的代码和数据更小了。
  2. 线程的调度成本更低了,是因为它将来在调度的时候,核心数据结构(地址空间和页表)均不用切换了。
  • 上述线程仅仅是在Linux下的实现,不同平台对线程管理可能不一样。
  1. 如Windows有真正的有关多线程的数据结构。
  2. 而Linux并没有真正的对线程创建对应的数据结构。
  3. Linux的线程是用进程PCB模拟的。
  4. 所以Linux并不能直接提供线程相关的接口,只能提供轻量级进程的接口。

              💦在用户层实现了一套用户层多线程方案,以库的方式提供给用户进行使用。

              💦pthread线程库。

  • CPU视角下,Linux下,PCB <= 其他OS内的PCB。
  1. Linux下的进程,统一称之为:轻量级进程。

💫 线程优点

  • 创建一个新线程的代价要比创建一个新进程小得多。
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
  • 线程占用的资源要比进程少很多。
  • 能充分利用多处理器的可并行数量。
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

💫 线程缺点

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

💫 线程异常

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

💫 线程用途

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

🌙 进程VS线程


💫 进程和线程

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

  • 线程是调度的基本单位

  1. 线程ID
  2. 一组寄存器
  3. errno
  4. 信号屏蔽字
  5. 调度优先级
  • 为什么线程切换的成本更低
  1. 地址空间和页表不需要切换。
  2. CPU内部是有L1~L3 cache,如果进程切换,cache就立即失效,新进程过来,只能重新缓存。

💫 进程和线程的资源共享

  • 进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的:

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

  • 除此之外,各线程还共享以下进程资源和环境:
  1. 文件描述符表。
  2. 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)。
  3. 当前工作目录。
  4. 用户id和组id。
  • 补充说明:
__thread int g_val = 100; // 修饰全局变量,让每一个线程各自拥有一个全局的变量  --  线程的局部存储

💫 进程和线程的关系

💫 关于进程线程的问题

如何看待之前学习的单进程?

具有一个线程执行流的进程。

引入线程后,如何重新理解之前的进程?

  • 红色方框框起来的内容,将这个整体称作进程
  • 曾经理解的进程 = 内核数据结构 + 进程对应的代码和数据
  • 现在的进程,从内核角度看:承担分配系统资源的基本实体

一个进程内部一定存在多个执行流,那么这些执行流在CPU角度有区别吗?

没有任何区别,CPU不关心当前是进程还是线程这样的概念,只关心PCB,CPU调度的时候照样以task_struct为单位来进行调度。

  • 只是这里task_struct背后的代码和页表只是曾经的代码和页表的一小部分而已。
  • 所以CPU执行的只是一小块代码和数据,但并不妨碍CPU执行其它执行流。
  • 所以就可以把原本串行的所有代码转变成并发或并行的,让这些代码在同一时间点得以推进。

总结如下:

以前CPU看到的所有的task_struct都是一个进程,现在CPU看到的所有的task_struct都是一个执行流(线程)

​🌙线程控制


💫 POSIX线程库

使用:

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”开头的
  • 要使用这些函数库,要通过引入头文<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

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

  • 我们说过,在Linux没有真正意义上的线程,无法直接提供创建线程的系统接口,只能给我们提供创建轻量级进程的接口。但是在用户的角度上,当我们想创建一个线程时会使用thread_create这样的接口,而不是我们上面所使用vfork函数,用户不能直接访问OS,所以OS在用户和系统调用之间提供了编写好的用户级线程库,这个库一般称为pthread库。任何Linux操作系统都必须默认携带这个库,这个库称为原生线程库。
  • 原生的线程库本质上就是对轻量级进程的系统调用(clone)进行了封装pthread_create,使用户层模拟实现了一套线程相关的接口。
  • 我们认为的线程实际在OS内部会被转化成我们所谓的轻量级进程。

错误检查:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小。

💫 创建线程——pthread_create

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:用于设置创建线程的属性,传入nullptr表示默认,这个属性基本不管
  • start_routine:函数地址,表示线程启动后要执行的函数
  • arg:传给线程例程的参数
  • 返回值:成功返回0,失败返回错误码

举个栗子:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
using namespace std;
void * thread_routine(void *args)
{
    const char*name = (const char*)args;
    while(true)
    {
        cout<<"这是新线程,我正在运行!"<<name<<endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,thread_routine,(void*)"thread one");
    assert(0==n);
    (void)n;

    while(true)
    {
        cout<<"这是主线程,我真正运行!"<<endl;
        sleep(1);
    }

    return 0;
}

这里编译运行需要注意:这个接口是库给我们提供的,使用的接口如果不是语言上的接口或者操作系统上的接口,如果是库提供的,那在编译时是不通过的,我们需要找到这个库。-L:找到库在哪里,-I:找到头文件在哪里,但是这个库已经在系统里安装好了,除了告诉库和头文件在哪之外,还需要知道链接哪一个库!

此时我们用ps axj命令查看当前进程的信息时,虽然此时该进程中有两个线程,但是我们看到的进程只有一个,因为这两个线程都是属于同一个进程的:

而使用ps -aL指令,就可以显示当前的轻量级进程了:

其中,LWP(Light Weight Process)表示的就是轻量级进程的ID,可以看到显示的两个轻量级进程的PID是相同的,因为它们是属于同一个进程的。每个轻量级进程都有唯一的LWP。

  • 注意:主线程的PID和LWP是一样的。不一样的就是新线程。所以CPU调度的时候,是以LWP为标识符表示特定一个执行流。
  • 线程一旦被创建,几乎所有的资源都是被所有线程共享的。所以线程之间想交互数据就容易了,直接就能看到。

线程也一定要有自己私有的资源:

  1. 线程被调度就要有独立的PCB属性私有。
  2. 线程切换时正在运行,需要进行上下文保存,要有私有的上下文结构。
  3. 每个进程都要独立的运行,每个线程都要有自己独立的栈结构。

主线程创建一批新线程:

我们让主线程一次性创建十个新线程,并让创建的每一个新线程都去执行start_routine函数,也就是说start_routine函数会被重复进入,即该函数是会被重入的:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;

class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
    
};

//创建一批新线程
void* start_routine(void* args)
{
    sleep(1);
    ThreadData *td = static_cast<ThreadData *>(args);
    int cnt = 10;
    while(cnt)
    {
        cout<<"new thread create success,name: "<<td->namebuffer<<" cnt : "<<cnt--<<endl;
        sleep(1);
    }
    delete td;
    return nullptr;
}
int main()
{
    vector<ThreadData*> threads;
#define NUM 10
    for(int i = 0;i<NUM;i++)
    {
        ThreadData *td = new ThreadData();
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
        pthread_create(&td->tid,nullptr,start_routine,td);
        threads.push_back(td);
       // sleep(1);
    }

    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    while(true)
    {
        cout<<"new thread create success,name: main thread"<<endl;
        sleep(1);
    }
    return 0;
}

并且start_routine是可重入函数,没有产生二义性,没有因为一个线程去影响另一个线程。并且在函数内定义的变量都是局部变量具有临时性,在多线程情况下也没有问题。这也说明了每一个线程都有自己独立的栈结构

获取线程ID——pthread_self:

获取线程ID:1.创建线程时通过输出型参数获得;2.通过pthread_self接口函数获得

#include <pthread.h>
pthread_t pthread_self(void);

我们可以打印出主线程打印出新线程的ID,新线程打印自己的ID,看是否相同:结果是相同的

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;

string changeId(const pthread_t &thread_id)
{
    char tid[128];
    snprintf(tid,sizeof(tid),"0x%x",thread_id);
    return tid;
}
void* start_routine(void*args)
{
    std::string threadname = static_cast<const char*>(args);
    while(true)
    {
        cout<<threadname<<" running ... "<<changeId(pthread_self())<<endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    
    cout<<"main thread running ... new thread id: "<<changeId(tid)<<endl;
    pthread_join(tid,nullptr);
    return 0;
}

💫 线程等待——pthread_join

概念:

一个线程创建出来,那就要如同进程一样,也是需要被等待的。如果线程不等待,对应的PCB没被释放,也会造成类似僵尸进程的问题:内存泄漏。所以线程也要被等待:

  1. 获取新线程的退出信息
  2. 回收新线程对应的PCB等内核资源,防止内存泄漏。

可以不关心线程的退出信息。

pthread_join:等待线程的函数

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
  • 参数:thread:被等待线程的ID,retval:线程退出时的退出码信息
  • void** retval:输出型参数,主要用来获取线程函数结束时返回的退出结果。之所以是void**,是因为如果想作为输出型结果返回,因为线程函数的返回结果是void*,而要把结果带出去就必须是void**,
  • 返回值:线程等待成功返回0,失败返回错误码

举个栗子:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;

class ThreadData
{
public:
    int number;
    pthread_t tid;
    char namebuffer[64];
    
};
class ThreadReturn
{
public:
    int exit_code;
    int exit_result;
};
//创建一批新线程
void* start_routine(void* args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    int cnt = 10;
    while(cnt)
    {
        cout<<"cnt:"<<cnt<<"&cnt:"<<&cnt<<endl;
        cnt--;
        sleep(1);
    }
   ThreadReturn* tr = new ThreadReturn();
   tr->exit_code = 1;//线程退出码
   tr->exit_result = 100;//线程退出结果
   return (void*)tr;
   //return (void*)td->number;//waring void*ret = (void*)td->number;8字节、4字节
}
int main()
{
    vector<ThreadData*> threads;
#define NUM 10
    for(int i = 0;i<NUM;i++)
    {
        ThreadData *td = new ThreadData();
        td->number = i+1;
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
        pthread_create(&td->tid,nullptr,start_routine,td);
        threads.push_back(td);
    }
    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    for(auto&iter:threads)
    {
        ThreadReturn*ret = nullptr;
        int n = pthread_join(iter->tid,(void**)&ret);
        assert(n==0);
        cout<<"join : "<<iter->namebuffer<<" success,exit_code: "<<ret->exit_code<<",exit_result: "<<ret->exit_result<<endl;
        delete iter;
    }
    cout<<"main thread quit"<<endl;
    return 0;
}

总结:

没有看到线程退出时对应的退出信号:这是因为线程出异常收到信号,整个进程都会退出,所以退出信号要由进程来关心,所以pthread_join默认会认为函数会调用成功,不考虑异常问题,异常问题是进程该考虑的问题。

💫 线程终止——return、pthread_exit、pthread_cancel

一个新创建出来的线程,如果想终止线程而不是整个进程,有三种做法:

  1. 直接从线程函数结束,return的时候,线程就算终止了。
  2. 线程可以自己调用pthread_exit函数终止自己。
  3. 一个线程可以调用pthread_cancel函数终止同一进程中的另一个线程。

return终止线程:

注意:exit不能用来终止线程,因为exit是来终止进程的。任何一个执行流调用exit都会让整个进程退出,所以终止线程不能采用exit,而是采用return来终止线程。

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;

class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
    
};
//创建一批新线程
void* start_routine(void* args)
{
    sleep(1);
    ThreadData *td = static_cast<ThreadData *>(args);
    int cnt = 10;
    while(cnt)
    {
        cout<<"cnt:"<<cnt<<"&cnt:"<<&cnt<<endl;
        cnt--;
        sleep(1);
        return nullptr;
    }
    delete td;
}

int main()
{
    vector<ThreadData*> threads;
#define NUM 10
    for(int i = 0;i<NUM;i++)
    {
        ThreadData *td = new ThreadData();
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
        pthread_create(&td->tid,nullptr,start_routine,td);
        threads.push_back(td);
    }
    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    while(true)
    {
        cout<<"new thread create success,name: main thread"<<endl;
        sleep(1);
    }
    return 0;
}#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;

class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
    
};
//创建一批新线程
void* start_routine(void* args)
{
    sleep(1);
    ThreadData *td = static_cast<ThreadData *>(args);
    int cnt = 10;
    while(cnt)
    {
        cout<<"cnt:"<<cnt<<"&cnt:"<<&cnt<<endl;
        cnt--;
        sleep(1);
        return nullptr;
    }
    delete td;
}

int main()
{
    vector<ThreadData*> threads;
#define NUM 10
    for(int i = 0;i<NUM;i++)
    {
        ThreadData *td = new ThreadData();
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
        pthread_create(&td->tid,nullptr,start_routine,td);
        threads.push_back(td);
    }
    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    while(true)
    {
        cout<<"new thread create success,name: main thread"<<endl;
        sleep(1);
    }
    return 0;
}

最终新建线程终止。

pthread_exit函数:

pthread_exit函数的功能就是终止线程:

#include <pthread.h>
void pthread_exit(void *retval);

retval:线程退出时的退出码信息,默认设置为nullptr

举个栗子:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;

class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
    
};
//创建一批新线程
void* start_routine(void* args)
{
    sleep(1);
    ThreadData *td = static_cast<ThreadData *>(args);
    int cnt = 10;
    while(cnt)
    {
        cout<<"cnt:"<<cnt<<"&cnt:"<<&cnt<<endl;
        cnt--;
        sleep(1);
    }
    delete td;
    pthread_exit(nullptr);
}
int main()
{
    vector<ThreadData*> threads;
#define NUM 10
    for(int i = 0;i<NUM;i++)
    {
        ThreadData *td = new ThreadData();
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
        pthread_create(&td->tid,nullptr,start_routine,td);
        threads.push_back(td);
    }
    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    while(true)
    {
        cout<<"new thread create success,name: main thread"<<endl;
        sleep(1);
    }
    return 0;
}

pthread_cancel:

线程是可以被其他线程取消的,但是线程要被取消,前提是这个线程是已经运行起来了。pthread_create取消也是线程终止的一种

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

我们以取消一半的线程为例:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;

class ThreadData
{
public:
    int number;
    pthread_t tid;
    char namebuffer[64];  
};
//创建一批新线程
void* start_routine(void* args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    int cnt = 10;
    while(cnt)
    {
        cout<<"cnt:"<<cnt<<"&cnt:"<<&cnt<<endl;
        cnt--;
        sleep(1);
    }
  return (void*)100;
}
int main()
{
    vector<ThreadData*> threads;
#define NUM 10
    for(int i = 0;i<NUM;i++)
    {
        ThreadData *td = new ThreadData();
        td->number = i+1;
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
        pthread_create(&td->tid,nullptr,start_routine,td);
        threads.push_back(td);
    }
    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    sleep(5);
    //取消一半的线程
    for(int i = 0;i<threads.size()/2;i++)
    {
        pthread_cancel(threads[i]->tid);
        cout<<"ptheread_cancel : "<<threads[i]->namebuffer<<" success"<<endl;
    }

    for(auto&iter:threads)
    {
        void*ret = nullptr;
        int n = pthread_join(iter->tid,(void**)&ret);
        assert(n==0);
        cout<<"join : "<<iter->namebuffer<<" success,exit_code: "<<(long long)ret<<endl;
        delete iter;
    }
    cout<<"main thread quit"<<endl;
    return 0;
}

线程如果是被取消的,退出码是-1,-1是一个宏,PTHREAD_CANCELED,我们可以查看定义:

#define PTHREAD_CANCELED ((void *) -1)

初步重新认识我们的线程库(语言版)

任何语言,在Linux中,如果要实现多线程,必定要使用pthread库,如何看待C++11中的多线程:C++11的多线程,在Linux环境中本质就是对pthread库的封装。

💫 分离线程——pthread_detach

概念:

线程是可以等待的,等待的时候,是join的等待的,阻塞式等待。而如果线程我们不想等待:不要等待,该去进行分离线程处理。默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏而如果我们不关心线程的返回值,join是一种负担,这个时候我们可以告诉OS,当线程退出时,自动释放线程资源,这种策略就是线程分离。

phread_detach使用:

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

下面我们创建新线程,让主线程与新线程运行起来,主线程等待新线程退出,等待完毕返回n,而现在让创建的新线程进行分离,按照我们的预料:此时应该是等待失败:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <vector>
using namespace std;

string changeId(const pthread_t & thread_id)
{
    char tid[128];
    snprintf(tid,sizeof(tid),"0x%x",thread_id);
    return tid;
}
void*start_routine(void*args)
{
    string threadname = static_cast<const char*>(args);
    pthread_detach(pthread_self());//线程分离,设置为分离状态
    int cnt = 5;
    while(cnt--)
    {
        cout<<threadname<<" running ... "<<changeId(pthread_self())<<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    string main_id = changeId(pthread_self());
    cout<<"main thread running... new thread id:"<<changeId(tid)<<"main thread id: "<<main_id<<endl;
    //一个线程默认是joinable的,设置了分离状态,不能够进行等待了
    int n = pthread_join(tid,nullptr);
    cout<<"result:"<<n<<": "<<strerror(n)<<endl;
    return 0;
}

🌟结束语 

       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

​​​ 

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

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

相关文章

如何使用前端表格控件实现多数据源整合?

前言 作为表格产品的典型应用场景之一&#xff0c;几乎所有的行业都会存在类 Excel 报表开发这样的应用场景&#xff0c;而在这些应用场景中&#xff0c;经常会遇见下面的这些痛点&#xff1a; 报表数据往往来自多个不同的数据源&#xff0c;需要报表系统能够同时连接多个数据源…

数字化转型推动生物技术企业增长—纷享销客与集萃药康共探新动力

上周&#xff0c;在南京锦创书城&#xff0c;一场主题为“生物技术企业增长新动力&#xff1a;以客户为中心的数字化转型与创新”的研讨会圆满落幕。此次活动由纷享销客江苏分公司联合江苏集萃药康生物科技股份有限公司共同举办&#xff0c;吸引了众多生物技术领域企业的负责人…

华媒舍:10种欧洲地区媒体发稿推广技巧

1.了解欧洲地区媒体自然环境必须掌握欧洲地区媒体的发稿推广方法&#xff0c;首先要对欧洲地区媒体自然环境有一定的了解。包含不一样国家的主力媒体&#xff0c;他的阅读者人群、销售市场遮盖及其报导风格等。仅有熟悉媒体自然环境&#xff0c;才能更好的制订营销推广策略。 …

电容、电阻、二极管和三极管的分类

目录 一、电容 1、符号 2、关键参数 &#xff08;1&#xff09;容值 &#xff08;2&#xff09;封装 &#xff08;3&#xff09;精度 &#xff08;4&#xff09;耐压值&#xff08;额定电压&#xff09; 3、作用 &#xff08;1&#xff09;储能 &#xff08;2&#xff…

使用opencv 进行车牌位置检测的源代码

效果: 这一个车牌识别系统中的预处理函数,其主要目的是对输入的车牌图片进行一系列的图像处理操作,以便后续的车牌识别算法能够更准确地识别出车牌。 整个函数的流程是:读取图像 -> 缩放 -> 灰度化 -> 去噪 -> 边缘检测 -> 形态学操作 -> 轮廓检测 ->…

SQL数据库多表创建之一对多、多对多表创建

MySQL多表创建关联及操作_mysql创建关联表-CSDN博客文章浏览阅读1.1k次&#xff0c;点赞21次&#xff0c;收藏20次。表与表之间的关系表语表之间的关系&#xff0c;说的就是表与表数据之间的关系。_mysql创建关联表https://blog.csdn.net/2401_83641392/article/details/137031…

苍穹外卖数据可视化

文章目录 1、用户统计2、订单统计3、销量排名Top10 1、用户统计 所谓用户统计&#xff0c;实际上统计的是用户的数量。通过折线图来展示&#xff0c;上面这根蓝色线代表的是用户总量&#xff0c;下边这根绿色线代表的是新增用户数量&#xff0c;是具体到每一天。所以说用户统计…

谷歌上架,可以用云手机来完成开发者个人号“20+14”封测?

根据谷歌的政策要求&#xff0c;自2023年11月13日之后创建的个人开发者账号&#xff0c;其应用必须满足20人连续14天封闭测试的要求&#xff0c;才能在Google Play 中上架正式版应用。 20人连续测试14天&#xff0c;这对大多数开发者&#xff0c;尤其是那些采用矩阵方式上架的开…

变现实谈,我要的不是灵光一现,而是真实的实现!——感悟篇

变现要的是行动不是想法 正文时代奇点奇迹 点题以己及人 正文 每当我看到了一个有趣的事情 我会在脑中构思一些想法 会贴合我当下的想要做的事情 比如 在我写下这篇文章之前 我看到了 二战期间的诞生的一个奇迹 可口可乐 我就思考 咦 原来可口可乐居然是在这么个时间点成长…

Vue.js2+Cesium1.103.0 十六、多模型轨迹运动

Vue.js2Cesium1.103.0 十六、多模型轨迹运动 Demo <template><div id"cesium-container" style"width: 100%; height: 100%;"><ul class"ul"><li v-for"(item, index) of deviceInfo" :key"index" cl…

SqlServer还原系统库步骤及问题解决

还原master 需要切换到binn目录 Cd C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\Binn 关闭服务 用单用户模式启动 SQL Server 默认实例 sqlservr.exe -m 直接单用户登录 恢复master备份文件 RESTORE DATABASE master FROM DISK E:\dbbak\txic_ke…

低代码与人工智能的深度融合:行业应用的广泛前景

引言 在当今快速变化的数字化时代&#xff0c;企业面临着越来越多的挑战和机遇。低代码平台和人工智能技术的兴起&#xff0c;为企业提供了新的解决方案&#xff0c;加速了应用开发和智能化转型的步伐。 低代码平台的基本概念及发展背景 低代码平台是一种软件开发方法&#x…

Golang | Leetcode Golang题解之第119题杨辉三角II

题目&#xff1a; 题解&#xff1a; func getRow(rowIndex int) []int {row : make([]int, rowIndex1)row[0] 1for i : 1; i < rowIndex; i {row[i] row[i-1] * (rowIndex - i 1) / i}return row }

python基础-数据结构-int类型——你知道python的最大整数是什么吗?无限大?还是sys.maxsize?

文章目录 int底层源码最大整数推断 int底层源码 python 的int类型貌似并没有一个位数上线&#xff0c;然而我们知道其他语言的整数都是有位数&#xff0c;一般为32位或者64位等&#xff0c;那么python是怎么实现int类型没有位数限制的呢&#xff0c;下面这段代码是cpython仓库…

ams仿真将一组输出波形转化为所对应的十进制code

virtuoso ADE XL ams仿真将一组输出波形转化为所对应的十进制code 选中一组波形 输入波形翻转电压&#xff0c;起个名字

复习leetcode第二题:两数相加

本文会给出笔者自己的解答&#xff08;代码较为冗余&#xff0c;其实就是屎山代码&#xff09;以及优秀代码的解析 下图是题目 解法1&#xff08;笔者所使用的办法&#xff09;&#xff1a; 解题思路&#xff1a; 以下思路是基于示例1&#xff08;上图&#xff09;思考的 步骤…

【JS红宝书学习笔记】第6章 集合引用类型

第6章 集合引用类型 对象 数组与定型数组 Map、WeakMap、Set 以及 WeakSet 类型 1. object 很适合存储和在应用程序间交换数据。 显式创建object的两种方式&#xff1a; &#xff08;1&#xff09;new操作符 let person new Object(); person.name "Nicholas";…

nginx的安装001

Nginx是一款高性能的HTTP和反向代理服务器&#xff0c;以及邮件代理服务器&#xff0c;由 Igor Sysoev 开发并公开发布于2004年。Nginx以其高并发处理能力、低内存消耗和稳定性著称&#xff0c;特别适合部署在高流量的网站上。 操作系统&#xff1a; CentOS Stream 9 安装步骤…

CentOS7配置国内清华源并安装docker-ce以及配置docker加速

说明 由于国内访问国外的网站包括docker网站&#xff0c;由于种种的原因经常打不开&#xff0c;或无法访问&#xff0c;所以替换成国内的软件源和国内镜像就是非常必要的了&#xff0c;这里整理了我安装配置的基本的步骤。 国内的软件源有很多&#xff0c;这里选择清华源作为…

官方小游戏项目

一 项目原理&#xff1a;看广告&#xff0c;操作简单&#xff0c;时间自由&#xff0c;适合利用业余时间来做&#xff0c;一个广告大概在15s-30s之间。 二 介绍&#xff1a;给你开代理权限&#xff0c;你就有独立后台管理系统&#xff0c;监测每台手机每条广告的情况&#xff0…