多线程——线程概念和线程控制

简介: 什么是线程,POSIX线程库,线程控制:pthread_create线程创建,pthread_exit线程终止,pthread_join线程回收,pthread_cancel线程取消,pthread_detach线程分离。线程id和地址空间分局,C++语言级别的多线程,二次封装线程库

多线程——线程概念和线程控制

位图 (6)

[TOC]

线程的概念

什么是线程

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

  2. 一切进程至少都有一个执行线程

  • 我们知道,一个进程被创建出来,伴随着一个进程控制块(task_struct),进程地址空间(mm_struct),页表的创建,虚拟地址通过页表与物理内存上的物理地址完成映射等等。实际上这一整个流程算是一个线程

image-20230708183037418

  • 当创建子进程时,实际上是OS另外创建了一个task_struct结构,然后分别拷贝了父进程的mm_struct,页表,接着建立虚拟地址到物理地址的映射等等。这也意味着进程具有自己独立的数据结构,保证了进程的独立性。

image-20230708183535600

  • 但当我们创建线程时,实际上只创建独立的task_struct,然后统一映射同一套mm_struct,页表和物理内存。其中每一个线程都算是一个执行流,也就是我们说的线程是进程内部的一个执行分支。
  • 线程在进程里面,这也保证了进程里的资源是线程之间共享的
  • 创建线程只需要创建独立的task_struct,其余资源由进程分配。所以我们说线程相比于原始进程是轻量级进程

image-20230708184033829

重新理解进程

  • 站在OS上看,一个进程包括task_struct,mm_struct,页表,信号,虚拟地址与物理内存的映射等等,进程作为承担分配系统资源的实体。而线程具有独立的task_struct,透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,线程可以占用虚拟地址空间的一部分。除了task_struct,执行流其余资源由进程分配,换言之看待线程要从task_struct上看起。在CPU眼中,无论是原始进程还是线程,也就是一个执行流和多个执行流的区别。而CPU看待进程只会看到task_struct,所以线程是CPU调度的基本单位。因此线程都要比传统的进程更加轻量化,

image-20230709113952739

  1. 线程在进程内部运行,本质是在进程地址空间内运行
  • 地址空间是进程能看到资源的窗口。进程地址空间把资源划分为内核区和用户区。其中用户区有栈,堆,共享区等等。资源属于哪个部分,pcb就在那个部分通过页表找到磁盘对应的资源。
    image-20230708121231636

  • 其中页表决定进程真正拥有资源的状况。

    页表有许多条目。32位系统下,物理内存是4G即2^32字节,即有2^32个地址。其中物理内存中被划分为许多页框(或者叫块),页框大小4KB。相应的磁盘也被划分为许多页帧,页帧大小也是4KB,这样OS将数据从磁盘加载到内存或内存保存到磁盘上就是以4KB为单位。回到内存,内存有2^32个地址,那么就有2^32个地址需要被映射。页表就需要建立2^32个逻辑地址与物理地址的映射。

image-20230708145148035

在页表中,每个条目不仅仅要有2^32位数字作为映射,还要有各种权限标志位,比如U/K标志位(User or Kernel)用来判断进程是处于用户态还是内核态。

image-20230708145902470

页表中存储物理地址和逻辑地址就需要8个字节(232个bit)另外加各种标志位一起算10个字节。那么一个页表就需要2^32 10字节即40G内存大小,很显然在32位系统下内存大小远远不够。所以就需要用到多级页表来进行映射。

一级页表被称为页目录,页目录的每个条目映射二级页表,二级页表可以称为页表,页表的每个条目映射物理的地址。

页目录拿着虚拟地址的高10位来映射页表,可以映射2^10个页表。页表内有页号标识,页目录能通过虚拟地址的高十位找到相应的页表。页表拿着虚拟地址的中10位来映射物理地址的起始位置,找到物理地址的起始位置后,再拿着虚拟地址的低12位作为物理地址的偏移量。前面提到物理内存被划分为一个个4KB大小的页框,2^12bit刚好为4KB,也就是刚好在页框内通过偏移量找到数据。

image-20230708160001505

每个页表的条目还是10字节,那么在二级页表的情况下,页表大小为12^10 10byte=1M,即一个页表有1个页目录,2^10个二级页表,每个二级页表10字节大小。实际上Linux下的页表也是这样映射的。

注意:对于32位的机器,采用二级页表是合适的;但对于64位的机器,采用二级页表是不合适的,因此必须采用多级页表。

另外多级页表还解决程序和数据无需连续存储空间的问题。若是一个页表内包含全部内容,那么在加载页表的时候除了所占空间巨大外,还需要开辟一段连续空间给页表,显然消耗是巨大的。而多级页表就能让页表分散化,无需占用大块的连续空间。

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

在页表视角看待段错误

当我们需要修改字符串常量时,通过页表找到相应字符串存在的页框,然而该进程对于该字符串的权限是只读,进程对该字符串做修改时违背了权限,OS识别到该进程发生了错误,立刻发送信号进行终止。

  • 因此,合理的对地址空间和页表进行资源划分,我们就能对一个进程的所有资源进行管理。
  1. 在Linux系统中,是没有线程的概念的,是通过进程来模拟线程即轻量级进程。

    无需建立线程有关的数据结构,杜绝了在同一个进程中造成线程与进程地址空间,页表,物理空间之间的映射和进程与进程地址空间,页表,物理空间之间的映射,避免了这两套映射之间带来的强耦合性。因此OS也无法提供给我们线程创建的相关接口,但能提供给我们轻量级线程创建的接口。

POSIX线程库

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

线程控制

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

pthread_create线程创建

函数原型如下

 #include <pthread.h>
 int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
 void *(*start_routine) (void *), void *arg);
  • 参数thread是线程id,这里传入的是id的地址,即新创建的线程id指向的内存单元。参数还是个输出型参数,若创建线程成功,
  • 参数attr是线程属性,通常设置为nullptr
  • 参数start_routine是线程调用的函数,这个参数是函数指针,函数参数类型为void,返回值类型为void ,即新创建的线程从start_routine函数的地址开始运行
  • 参数arg是线程调用函数的参数,参数类型是void*。若函数start_routine需要参数,将参数放进某个结构中,然后将结构的地址arg传入
  • 调用成功返回0,失败返回对应错误码

需要注意的是,pthread并非是Linux系统的默认库,需要手动连接线程库 -lpthread。而且线程库在根目录底下的lib64路径底下。实际上这个线程库是系统安装自带的,被称为原生线程库

makefile文件

mythread:mythread.cc
    g++ -o {
   
   mathJaxContainer[0]}^ -std=c++11 -lpthread

PHONY:clean
clean:
    rm -rf mythread

mythread.cc

#include<iostream>
#include<string.h>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;
void* start_rontine(void* args)
{
   
   
    string str=static_cast<const char*>(args);//static_cast<const char*>的作用是参数args进行从void*转变为const char*的类型转化时进行检查,一般用于相近类型的安全转化
    while(true)
    {
   
   
        cout<<"我是一个新线程,我的参数是:"<<str<<endl;
        sleep(1);
    }
}
int main()
{
   
   
    pthread_t id;//创建一个新的线程id
    int n=pthread_create(&id,nullptr,start_rontine,(void*)"new_pthread");
assert(n==0);
while(true)
{
   
   
    cout<<"我是主线程"<<endl;
    sleep(1);
}
    return 0;
}

image-20230709160046623

可以看到,本来是只有main函数一个执行流即主线程,然后创建了一个新线程

image-20230709163727853

  • 通过查看看到,有两个相同PID的进程,但LWP不同。这里指的是同一个进程里存在两个执行流,执行流之间以LWP(Light Weight Process)作为分辨。并且线程LWP与进程PID相同的那个线程是主线程。意味着,当进程里只有一个执行流时,线程LWP与进程PID相同。这里需要注意的是,当我们发送信号的时候要向进程PID发送信号而不能向线程LWP发送。
  • 实际上pthread_create底层调用的是系统调用clone

clone创建子进程,函数原型:

 #include <sched.h>

int clone(int (*fn)(void *), void *child_stack,int flags, void *arg);
  • 参数fn是函数指针,指针指向子进程所调用的函数
  • 参数child_stack是一个指针,指向子进程分配到的系统堆栈空间
  • 参数flags标志位用来描述子进程需要从父进程继承哪些资源
  • arg是传给子进程执行fn函数的参数

需要注意的是,该子进程是指轻量级进程。另外函数fork创建的子进程并不和父进程共享进程地址空间,函数vfork创建的子进程与父进程共享进程地址空间

线程的资源分配

前面提到线程能够访问进程内的资源,线程能够共享进程的资源有代码段、数据段、文件描述符表、信号的处理方式、当前工作目录、用户id和组id等

这里我设置了一个全局变量g_val和一个fun函数,可以看到两个线程都能访问g_val和fun函数

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

using namespace std;
int g_val=0;

string fun()
{
   
   
    return "我是一个fun函数";
}

void* start_rontine(void* args)
{
   
   
    string str=static_cast<const char*>(args);
    while(true)
    {
   
   
        cout<<"我是一个新线程,我的参数是:"<<str<<" g_val:"<<g_val<<" "<<fun()<<endl;
        sleep(1);
    }

}

int main()
{
   
   
    pthread_t id;//创建一个新的线程id
    int n=pthread_create(&id,nullptr,start_rontine,(void*)"new_pthread");
assert(n==0);
while(true)
{
   
   
    cout<<"我是主线程"<<" g_val:"<<g_val++<<" "<<fun()<<endl;
    sleep(1);
}
    return 0;
}

image-20230709165748277

  • 可以看到全局变量g_val在主线程中被自增,在新线程打印。g_val在主线程被改变,新线程中也随之改变,得知全局变量是线程间共享的。

需要注意的是,线程共享进程数据,但也有私有部分:

  • 线程ID、一组寄存器、栈、errno、信号屏蔽字、调度优先级
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
using namespace std;
class threadData//建立结构体
{
   
   
 public:
int _num;
char _buffer[64];
};

void* start_rontine(void* args)
{
   
   
   threadData* td=static_cast<threadData*>(args);
   int cnt=3;//在线程内定义一个局部变量--存储在线程的独立栈-具有独立性
    while(cnt--)
    {
   
   
       cout<<"我是一个新线程,"<<td->_buffer<<"线程内自增变量cnt: "<<cnt<<endl;
        sleep(1);
    }

}
#define NUM 3
int main()
{
   
   
    for(int i=0;i<NUM;i++)
{
   
   
 pthread_t id;//创建一个新的线程id
    threadData* ta=new threadData();
    ta->_num=i;
    snprintf(ta->_buffer,sizeof ta->_buffer,"%s:%d","newthread",ta->_num);
    int n=pthread_create(&id,nullptr,start_rontine,(void*)ta);
    assert(n==0);
}   

while(true)
{
   
   
   cout<<"我是主线程,"<<endl;
    sleep(1);
}
    return 0;
}
  • 在这里我创建了三个新线程,还创建了一个threadData结构体,在main函数中,for循环体内每次循环都创建一个新的threadData临时对象,然后传参给start_rontine函数供新线程调用,即每个新线程调用的参数是不一样的。
  • 通过打印可以看出,在线程内变量cnt自增,不会影响别的线程内的变量cnt,并且不同线程的cnt地址也不相同,即线程内的变量具有独立性。实际上线程内的变量存储在线程的子栈结构中,即线程具有自己独立的栈结构。

image-20230710190749455

线程的优点

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

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

一是在切换进程时,需要切换进程的task_struct,页表,并且在CPU中切换进程的上下文。而切换线程时,只需要切换线程的task_struct,在CPU中切换线程上下文

二是在CPU中有一个区域叫做cache,即硬件级缓存,该区域的加载速度比CPU慢,但比内存快。当进程或线程需要被CPU调度时,OS会将热点数据(可能多次被调度处理的数据)加载到cache中。CPU调度数据,会先去cache中寻找,指定数据存在,即直接调度。若不存在,OS就到内存中将指定数据加载到cache中再调度。若当前是线程切换,除了切换线程上下文外,cache中的热点数据不会失效,继续供新切换进来的线程调度;若当前是进程切换,那么cache中热点数据会失效,除了切换进程上下文外,还需要切换cache中的热点数据。这个是线程切换比进程切换消耗更少的主要原因。

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

计算型密集型应用:主要是表现为调度CPU,如文件的加密解密,算法等

I/O密集型应用:主要表现为访问外设资源,如访问磁盘,显示器,网络等

线程的缺点

  • 性能有所损失

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

  • 程序健壮性降低

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

#include<iostream>
#include<string.h>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;
void* start_rontine(void* args)
{
   
   
    string str=static_cast<const char*>(args);
    while(true)
    {
   
   
       cout<<"我是一个新线程,"<<endl;
       int* ptr=NULL;
       *ptr=0;//空指针解引用--报错
        sleep(1);
    }
}

int main()
{
   
   
    pthread_t id;//创建一个新的线程id
    int n=pthread_create(&id,nullptr,start_rontine,(void*)"new_pthread");
assert(n==0);
while(true)
{
   
   
   cout<<"我是主线程,"<<endl;
    sleep(1);
}
    return 0;
}

我是主线程,
我是一个新线程,
Segmentation fault

这里在新线程发生了空指针解引用。空指针一般指向进程的最小的地址,通常这个值为0,当进程试图通过空指针对该数据进行访问,将会发生空指针解引用段错误。值得注意,新线程引发段错误,OS向新线程所在的进程发送信号来终止,那么新线程和主线程赖以利用的资源将会被进程回收,以至于线程都被终止了。

  • 缺乏访问控制

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

  • 编程难度提高

编写与调试一个多线程程序比单线程程序困难得多

pthread_exit线程终止

函数原型:

#include <pthread.h>
void pthread_exit(void *retval);
  • 参数retval 是 void* 类型的指针,可以指向任何类型的数据,它指向的数据将作为线程退出时的返回值。如果线程不需要返回任何数据,将 retval 参数置为 NULL 即可。
  • 调用成功返回0,失败返回错误码
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
using namespace std;
class threadData//建立结构体
{
   
   
 public:
int _num;
char _buffer[64];
};

void* start_rontine(void* args)
{
   
   
   threadData* td=static_cast<threadData*>(args);
   int cnt=3;//在线程内定义一个局部变量--存储在线程的独立栈-具有独立性
    while(cnt--)
    {
   
   
       cout<<"我是一个新线程,"<<td->_buffer<<"线程内自增变量cnt: "<<cnt<<endl;
        sleep(1);
        delete td;//释放指向的结构体资源
        pthread_exit(nullptr);//线程终止
    }

}
#define NUM 3
int main()
{
   
   
    for(int i=0;i<NUM;i++)
{
   
   
 pthread_t id;//创建一个新的线程id
    threadData* ta=new threadData();
    ta->_num=i;
    snprintf(ta->_buffer,sizeof ta->_buffer,"%s:%d","newthread",ta->_num);
    int n=pthread_create(&id,nullptr,start_rontine,(void*)ta);
    assert(n==0);
}   

while(true)
{
   
   
   cout<<"我是主线程,"<<endl;
    sleep(1);
}
    return 0;
}
  • 在新线程的循环体中,使用pthread_exit函数进行线程终止,通过打印看到新线程都进入一次循环体然后直接终止了,线程终止不会影响主线程的执行。

image-20230710184348752

另外还有两种方法可以只终止某个线程而不终止整个进程

  • 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  • 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_join线程回收

实际上线程也是需要被等待回收的,否则会造成类似僵尸进程问题,引发内存泄漏

函数原型

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
  • 参数thread为要等待的线程id
  • 参数retval 是 void* 类型的指针,可以指向任何类型的数据,它指向的数据将作为线程退出时的返回值。如果线程不需要返回任何数据,将 retval 参数置为 NULL 即可。
  • 调用成功返回0,失败返回错误码
  • join等待是阻塞式等待,主线程阻塞等待回收新线程
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
//#include"mythread.hpp"
using namespace std;

class threadData//建立结构体
{
   
   
    public:
pthread_t _pid;//线程id
int _num;
char _buffer[64];
};

void* start_rontine(void* args)
{
   
   
   // string str=static_cast<const char*>(args);
   threadData* td=static_cast<threadData*>(args);
   int cnt=3;//在线程内定义一个局部变量--存储在线程的独立栈-具有独立性
    while(cnt--)
    {
   
   
       cout<<"我是一个新线程,"<<td->_buffer<<"线程内自增变量cnt: "<<cnt<<"&cnt:"<<&cnt<<endl;
        sleep(1);
       //  pthread_exit(nullptr);
    }


}
#define NUM 3
int main()
{
   
   
    vector<threadData*> threads;
    for(int i=0;i<NUM;i++)
{
   
   
 pthread_t id;//创建一个新的线程id
    threadData* ta=new threadData();
    ta->_num=i;
    snprintf(ta->_buffer,sizeof ta->_buffer,"%s:%d","newthread",ta->_num);
    threads.push_back(ta);
    int n=pthread_create(&ta->_pid,nullptr,start_rontine,(void*)ta);
    assert(n==0);
} 
for(const auto &it:threads)
{
   
   

   int n= pthread_join(it->_pid,nullptr);//等待回收线程
     assert(n==0);
     cout<<"newpthread "<<it->_num<<" join success"<<endl;
    delete it;
}
while(true)
{
   
   
   cout<<"我是主线程,"<<endl;
    sleep(1);
}
    return 0;
}
  • 在这里先用vector将结构体对象存起来供后续调用对象结构体里的线程id变量,待线程执行完后,依次回收线程

image-20230710203933383

线程回收的作用:一是获取新线程的退出信息,二是回收新线程对应PCB等内核资源,防止内存泄漏

线程的返回值

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
using namespace std;

class threadData//建立结构体
{
   
   
    public:
pthread_t _pid;//线程id
int _num;
char _buffer[64];
};

void* start_rontine(void* args)
{
   
   
   threadData* td=static_cast<threadData*>(args);
   int cnt=3;//在线程内定义一个局部变量--存储在线程的独立栈-具有独立性
    while(cnt--)
    {
   
   
       cout<<"我是一个新线程,"<<td->_buffer<<"线程内自增变量cnt: "<<cnt<<"&cnt:"<<&cnt<<endl;
        sleep(1);
    }
   return (void*)520;

}
#define NUM 3
int main()
{
   
   
    vector<threadData*> threads;
    for(int i=0;i<NUM;i++)
{
   
   
 pthread_t id;//创建一个新的线程id
    threadData* ta=new threadData();
    ta->_num=i;
    snprintf(ta->_buffer,sizeof ta->_buffer,"%s:%d","newthread",ta->_num);
    threads.push_back(ta);
    int n=pthread_create(&ta->_pid,nullptr,start_rontine,(void*)ta);
    assert(n==0);
} 
   void* ret;//初始化
for(const auto &it:threads)
{
   
   

   int n= pthread_join(it->_pid,&ret);//等待回收线程
     assert(n==0);
     cout<<"newpthread "<<it->_num<<" join success"<<"return val:"<<(long long)ret<<endl;//Linux下void*大小为8个字节,所以强转为整形要用到long long
     delete it;
}
while(true)
{
   
   
   cout<<"我是主线程,"<<endl;
    sleep(1);
}
    return 0;
}

在前面的pthread_join函数都提到了线程退出的返回值。可以看到返回值类型是void*。实际上线程调用的函数返回值是void ,线程库中有一个专门接收该返回值的变量ret类型也是void ,当线程退出时,OS会将线程退出的返回值拷贝给线程库中的ret变量;但由于我们无法直接获取存在于线程库的变量ret,我们需要对ret取地址,然后解引用才能拿到该数据,对void 取地址的类型为void

image-20230710212422765

image-20230710212708704

可以看到新线程的返回值给pthread_join函数接收并打印出来了。实际上返回值是对象也能接收,只需要在退出的线程处先强转成void*类型,然后在外部接收的函数处做相应的强转即可

pthread_cancel线程的取消

函数原型

 #include <pthread.h>
 int pthread_cancel(pthread_t thread);
  • 参数thread是要取消线程的线程id
  • 调用成功返回0,失败返回错误码

线程要取消,前提是线程已经开始执行了,若线程被取消了,则退出码为-1,那么在回收线程时接收的返回值就为-1

#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
//#include"mythread.hpp"
using namespace std;

class threadData//建立结构体
{
   
   
    public:
pthread_t _pid;//线程id
int _num;
char _buffer[64];
};

void* start_rontine(void* args)
{
   
   
   threadData* td=static_cast<threadData*>(args);
   int cnt=3;//在线程内定义一个局部变量--存储在线程的独立栈-具有独立性
    while(cnt--)
    {
   
   
       cout<<"我是一个新线程,"<<td->_buffer<<"线程内自增变量cnt: "<<cnt<<"&cnt:"<<&cnt<<endl;
        sleep(1);
    }
   return (void*)520;

}
#define NUM 3
int main()
{
   
   
    vector<threadData*> threads;
    for(int i=0;i<NUM;i++)
{
   
   
 pthread_t id;//创建一个新的线程id
    threadData* ta=new threadData();
    ta->_num=i;
    snprintf(ta->_buffer,sizeof ta->_buffer,"%s:%d","newthread",ta->_num);
    threads.push_back(ta);
    int n=pthread_create(&ta->_pid,nullptr,start_rontine,(void*)ta);
    assert(n==0);
} 
sleep(2);//让新线程执行,两秒后取消新线程
for(const auto& it:threads)
{
   
   
    int n=pthread_cancel(it->_pid);
    assert(n==0);
    cout<<"new thread "<<it->_num<<"cancel success"<<endl;
}
   void* ret;//初始化
for(const auto &it:threads)
{
   
   

   int n= pthread_join(it->_pid,&ret);//等待回收线程
     assert(n==0);
     cout<<"newpthread "<<it->_num<<" join success"<<"return val:"<<(long long)ret<<endl;//Linux下void*大小为8个字节,所以强转为整形要用到long long
     delete it;
}
while(true)
{
   
   
   cout<<"我是主线程,"<<endl;
    sleep(1);
}
    return 0;
}

image-20230710232030040

一般线程取消是给主线程来控制新线程的,而取消新线程是不会影响主线程的

pthread_detach线程分离

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

  • 但如果不关心线程的返回值,join是一种负担,这个时候可以告诉系统:当线程退出时,自动释放线程资源即对目标线程进行分离

函数原型

#include <pthread.h>
int pthread_detach(pthread_t thread);
  • 参数thread是要分离线程的线程id
  • 调用成功返回0,失败返回错误码

  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离

  • 需要注意的是,joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
void* start_rontine(void* arg)
{
   
   
    string threadname=static_cast<const char*> (arg);
    cout<<"new thread name:"<<threadname<<endl;
    pthread_detach(pthread_self());//新线程自己让自己分离
    int cnt=3;
    while(cnt--)
    {
   
   
        cout<<"new thread run..."<<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
   
   
pthread_t id;
int n=pthread_create(&id,nullptr,start_rontine,(void*)"thread1");//创建新线程
assert(n==0);
int m=pthread_join(id,nullptr);//阻塞等待回收线程
assert(m==0);
cout<<"new thread ret:"<<n<<":"<<strerror(n)<<endl;//若新线程分离成功则主线程回收新线程失败
while(true)
{
   
   
    cout<<"main thread"<<endl;
    sleep(1);
}
    return 0;
}

image-20230711233631949

  • 上面的代码是错误写法。因为线程的joinable属性和分离是冲突的,而主线程和新线程的执行顺序由OS调度器决定,有可能还没执行新线程的pthread_detach函数进行线程分离之前,主线程已经执行到pthread_join函数,使得新线程具备了joinable属性,导致主线程已经在阻塞等待新线程,进而导致线程分离失败。

因此在回收新线程之前需要一定的时间,让新线程先执行到pthread_detach函数,使得新线程分离成功

正确写法

void* start_rontine(void* arg)
{
   
   
    string threadname=static_cast<const char*> (arg);
    int cnt=3;
    while(cnt--)
    {
   
   
cout<<"new thread name:"<<threadname<<endl;
sleep(1);
    }

    return nullptr;
}
int main()
{
   
   
pthread_t id;
int n=pthread_create(&id,nullptr,start_rontine,(void*)"thread1");//创建新线程
assert(n==0);
 pthread_detach(id);//线程分离--在主线程对新线程进行分离

int m=pthread_join(id,nullptr);//阻塞等待回收线程

cout<<"new thread ret:"<<m<<":"<<strerror(m)<<endl;//若新线程分离成功则主线程回收新线程失败
while(true)
{
   
   
    cout<<"main thread"<<endl;
    sleep(1);
}
    return 0;
}

image-20230712100947481

  • 在主线程对新线程分离,避免了因为新线程和主线程调度顺序不确定而引发了线程分离失败。

线程id以及地址空间布局

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的LWP不是一回事。
  • 内核中的LWP属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要 一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID, 属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 程库NPTL提供了pthread_ self函数,可以获得线程自身的ID,且获取到的ID和pthread_create的第一个参数获取到的ID是同一个

函数原型

#include <pthread.h>
pthread_t pthread_self(void);
  • 不需要传参,即获取当前调用函数线程的线程id

Linux不提供线程的内核数据结构,只提供LWP,意味着Linux只需要对LWP对应的执行流进行调度或管理,而提供给用户使用的用户级数据由线程库提供,即由线程库来管理。

  • 通过ldd可以看到,线程库是磁盘上的文件

image-20230712133459171

  • 线程库内有部分线程的数据结构,如 struct pthread,当中包含了对应线程的各种属性;线程局部存储,当中包含了对应线程被切换时的上下文数据;线程栈。这部分线程属性存在库中,被称为用户级线程。线程的其余数据结构或资源由OS提供,并且线程由OS调度,这部分线程被称为内核级线程,用户级线程:内核级线程=1:1
  • 每创建出一个线程,线程库中就具有一个这样的用户级线程数据结构,其中线程ID指向线程数据结构的首地址,即线程ID,这里与pthread_self获取的线程ID, pthread_create获取的第一个参数是一样的。

  • 线程栈位于线程库中,然后通过页表映射到进程地址空间的共享区中。也就是说主线程的栈位于进程地址空间的栈区,而新线程的栈位于共享区。

我们也可以通过地址的方式将线程id进行打印:

string changeID(const pthread_t&thread_id)
{
   
   
    char id[128];
    snprintf(id,sizeof id,"0x%x",thread_id);//以十六进制的方式将参数打印进id缓冲区并返回
    return id;
}

void* start_rontine(void* arg)
{
   
   
    string threadname=static_cast<const char*> (arg);

    int cnt=3;
    while(cnt--)
    {
   
   
cout<<"pthread_self get newthreadid: "<<threadname<<changeID(pthread_self())<<endl;//pthread_self获取的线程id
sleep(1);
    }

    return nullptr;
}
int main()
{
   
   
pthread_t id;
int n=pthread_create(&id,nullptr,start_rontine,(void*)"thread1");//创建新线程
assert(n==0);
char newthreadid[128];
snprintf(newthreadid,sizeof newthreadid,"0x%x",id);
cout<<"new threadid: "<<newthreadid<<endl;//pthread_create获取的线程id
 pthread_detach(id);//线程分离

int m=pthread_join(id,nullptr);//阻塞等待回收线程

cout<<"new thread ret:"<<m<<":"<<strerror(m)<<endl;//若新线程分离成功则主线程回收新线程失败
while(true)
{
   
   
    cout<<"main thread"<<endl;
    sleep(1);
}
    return 0;
}

image-20230712133748529

C++的多线程

实际上任何语音都能在Linux下实现多线程,前提是要使用线程原生库pthread。C++的多线程,本质是对pthread线程库的封装。

在Linux下实现简单的C++多线程

makefile

mythread:mythread.cc
    g++ -o {
   
   mathJaxContainer[1]}^ -std=c++11 -lpthread

PHONY:clean
clean:
    rm -rf mythread

thread.cc

#include<iostream>
#include<unistd.h>
#include<thread>

using namespace std;

void thread_tun()
{
   
   
    while(true)
    {
   
   
        cout<<"我是新线程"<<endl;
        sleep(1);
    }
}

int main()
{
   
   
    thread t1(thread_tun);
    while(true)
    {
   
   
        cout<<"我是主线程"<<endl;
        sleep(1);
    }
    t1.join();
    return 0;
}

image-20230710233119032

二次封装原生线程库

mythread.hpp

#include<iostream>
#include<pthread.h>
#include<string.h>
#include<functional>
using namespace std;


class thread;//声明
class Context
{
   
   
 public:
 thread* _this;//this指针
 void* _args;//函数参数
 public:
 Context()
 :_this(nullptr)
 ,_args(nullptr)
 {
   
   }
 ~Context()
 {
   
   }
};

class thread
{
   
   
public:
typedef  function<void* (void*)> func_t;//包装器构建返回值类型为void* 参数类型为void* 的函数类型
const int num=1024;
 thread(func_t func,void* args,int number=0)//构造函数
 : fun_(func)
 ,args_(args)
 {
   
   
char namebuffer[num];
snprintf(namebuffer,sizeof namebuffer,"threa--%d",number);//缓冲区内保存线程的名字即几号线程
Context* ctx=new Context();//
ctx->_this=this;
ctx->_args=args_;
int n=pthread_create(&pid_,nullptr,start_rontine,ctx);//因为调用函数start_rontine是类内函数,具有缺省参数this指针,在后续解包参数包会出问题,所以需要一个类来直接获取函数参数
assert(n==0);
(void)n;
 }

static void* start_rontine(void* args)
{
   
   
Context* ctx=static_cast<Context*>(args);
void *ret= ctx->_this->run(ctx->_args);//调用外部函数
delete ctx;
return ret;
}
void* run(void* args)
{
   
   
    return fun_(args);//调用外部函数
}
void join()
{
   
   
  int n=  pthread_join(pid_,nullptr);
  assert(n==0);
  (void)n;
}
~thread()
{
   
   
    //
}

    private:
    string name_;//线程的名字
    pthread_t pid_;//线程id
  func_t fun_;//线程调用的函数对象
  void* args_;//线程调用的函数的参数
};

thread.cc

#include<iostream>
#include<string.h>
#include<pthread.h>
#include<stdio.h>
#include<vector>
#include<assert.h>
#include<unistd.h>
#include<memory>
#include"mythread.hpp"
using namespace std;

void* getticket(void*args)
{
   
   
    string username=static_cast<const char*> (args);
   int cnt=3;
   while(cnt--)
    {
   
   
        cout<<"User name:"<<username<<"get tickets ing..."<<endl;
        sleep(1);
    }
}

int main()
{
   
   
unique_ptr<thread> thread1(new thread(getticket,(void*)"user1",1));
unique_ptr<thread> thread2(new thread(getticket,(void*)"user2",2));
unique_ptr<thread> thread3(new thread(getticket,(void*)"user3",3));
thread1->join();
thread2->join();
thread3->join();
    return 0;
}

image-20230712150908036

目录
相关文章
|
14天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
13 3
|
14天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
12 2
|
14天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
27 2
|
14天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
26 1
|
14天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
26 1
|
14天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
21 1
|
27天前
|
存储 运维 NoSQL
Redis为什么最开始被设计成单线程而不是多线程
总之,Redis采用单线程设计是基于对系统特性的深刻洞察和权衡的结果。这种设计不仅保持了Redis的高性能,还确保了其代码的简洁性、可维护性以及部署的便捷性,使之成为众多应用场景下的首选数据存储解决方案。
38 1
|
29天前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
38 1
C++ 多线程之初识多线程
|
2月前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
29天前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
43 6