【Linux】线程

线程

我们常常会在linux中或者在操作系统这门课中听到进程和线程的名称,我们之前认识了进程的概念,现在我们来了解一下线程的概念

线程概念:

什么是线程

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

    列”

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

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

  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化

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

    执行流

我们来看看我们之前进程的结构:

image-20231213132903039

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

当我们每次只创建“进程”(线程),只创建task_struct,让他们共享同一份进场地址空间和页表时,就会出现下面的场景:
image-20231213133307226

现在我们不需要为线程创建地址空间和页表,只需要为他们task_struct,这样我们就可以大大的节省内存空间

所以这四个task_struct看到的资源都是一样的,我们后续可以通过某种方式把代码区拆分成4块,让这四个task_struct执行不同的代码区域,上述的区域(数据区,堆区,栈区)也是类似处理方式。换言之,我们后续创建的3个task_struct都各自有自己的一小份代码和数据,我们把这样的一份task_struct称之为线程。

  • 线程其实就是一个当前进程的执行分支(执行流)

  • 线程在进程内部,而进程在进程地址空间中运行,其实本质上线程也在进场地址空间中运行

  • 线程所需的内存更小,所以在CPU眼中更加轻量级!

  • 线程的调度成本更低了,是因为它将来在调度的时候,核心数据结构(地址空间和页表)均不用切换了

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

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

  • 不用单独设计tcb了(Linux认为tcb和pcb的属性上很大部分重叠了,不需要单独设计pcb)
  • 不用维护tcb和pcb之间的关系了。
  • 不用在编写任何调度算法了。

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

答案是没有任何区别,CPU调度的时候照样以task_struct为单位来进行调度,只是这里task_struct背后的代码和页表只是曾经的代码和页表的一小部分而已。所以CPU执行的只是一小块代码和数据,但并不妨碍CPU执行其它执行流。所以我们就可以把原本串行的所有代码而转变成并发或并行的让这些代码在同一时间点得以推进。总结如下:以前CPU看到的所有的task_struct都是一个进程,现在CPU看到的所有的task_struct都是一个执行流(线程)
总览如下:

image-20231213134325964

接下来我们来看一个问题:下面这份代码是对的吗?肯定是错的啊!

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

我们以前常常说常量字符串不可以被修改!因为它其实被const修饰!

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

重新理解进程:

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

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

image-20231213135018568

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

image-20231213135120699

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

image-20231213135141397

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

简单使用线程

这里我们先简单套用两个函数pthread_create(创建线程)以及pthread_join(等待线程)

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

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

等待线程的函数叫做pthread_join,函数原型如下:

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

来看线程的运行:

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
using namespace std;
void* cctalk1(void *args)
{  
    string name=(char*)args;
    while(true)
    {
        cout<<name<<endl;
        sleep(1);
    }
}
void* cctalk2(void *args)
{   
    string name=(char*)args;
    while(true)
    {
        cout<<name<<endl;
        sleep(1);
    }}
int main()
{
    pthread_t t1;
    pthread_t t2;
    pthread_create(&t1,nullptr,cctalk1,(void*)"thread 1");
    pthread_create(&t2,nullptr,cctalk2,(void*)"thread 2");
    
    while(true)
    {
        cout<<"我是主线程..."<<endl;
        sleep(1);
    }
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);

    return 0;
}

我们可以发现当前有三个执行流(线程)

但是我们查看进程信息只能看到一个(这是正常现象,符号我们的预期),我们使用指令ps -aL就可以查到线程了,注意:LWP就是轻量级进程ID,当LWP和PID相同时,就说明其是主线程。

  • 默认情况下,不带 -L,看到的就是一个个的进程
  • 带 -L 就可以查看到每隔进程内的多个轻量级进程

所以,上述3个执行流的PID相同,说明3个线程属于同一个进程,而那个PID和LWP都相同的就是主线程。

注意:在Linux中,应用层的线程与内核的LWP是一一对应的,实际上操作系统调度的时候采用的是LWP,而并非PID,只不过我们之前接触到的都是单线程进程,其PID和LWP是相等的,所以对于单线程进程来说,调度时采用PID和LWP是一样的。

我们去试着打印他们的pid,我们发现他们三个的pid是相同,证明了三个执行流是在同一个进程内部的!!!

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
using namespace std;
void* cctalk1(void *args)
{  
    string name=(char*)args;
    while(true)
    {
        cout<<name<<":"<<getpid()<<endl;
        sleep(1);
    }
}
void* cctalk2(void *args)
{   
    string name=(char*)args;
    while(true)
    {
        cout<<name<<":"<<getpid()<<endl;
        sleep(1);
    }}
int main()
{
    pthread_t t1;
    pthread_t t2;
    pthread_create(&t1,nullptr,cctalk1,(void*)"thread 1");
    pthread_create(&t2,nullptr,cctalk2,(void*)"thread 2");
    
    while(true)
    {
        cout<<"我是主线程..."<<getpid()<<endl;
        sleep(1);
    }
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);

    return 0;
}

image-20231213142115909

二级页表:

我们以32位平台为例,在32位平台下一共有4G空间,我们对物理内存进行分页,一个页表4KB

那么需要多少条目(页表项)呢?答案是1mb个条目

image-20231213142723072

页表其实除了虚拟地址和物理地址还有一些关于权限的一些信息:

image-20231213143218376

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

image-20231213143702256

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

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

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

image-20231213144255936

如果把整个页旋转一下,把页目录放上面,就相当于一颗多叉树。

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

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

线程的优点

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

线程的缺点

性能损失

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

健壮性降低

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

缺乏访问控制

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

编程难度提高

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

线程异常

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

线程用途

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

Linux进程VS线程

进程和线程

进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:

线程ID
一组寄存器
栈
errno
信号屏蔽字
调度优先级

进程的多个线程共享

同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程
中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  1. 文件描述符表
  2. 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  3. 当前工作目录
  4. 用户id和组id

进程和线程的关系如下图:image-20231213145717743

Linux线程控制

POSIX线程库

在Linux中,站在内核角度没有真正意义上线程相关的接口,但是站在用户角度,当用户想创建一个线程时更期望使用thread_create这样类似的接口,而不是vfork函数,因此系统为用户层提供了原生线程库pthread。
原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口。
因此对于我们来讲,在Linux下学习线程实际上就是学习在用户层模拟实现的这一套接口,而并非操作系统的接

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的

  • 要使用这些函数库,要通过引入头文<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的“-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,该参数是一个输出型参数。

  • attr:用于设置创建线程的属性,传入NULL表示使用默认属性。

  • start_routine:返回值和参数均为void*的函数指针。该参数表示线程例程,即线程启动后要执行的函数。

  • arg:传给线程例程的参数。

返回值说明:

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

注意:

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

当一个程序启动时,就有一个进程被操作系统创建,与此同时一个线程也立刻运行,这个线程就叫做主线程。

主线程是产生其他子线程的线程。
通常主线程必须最后完成某些执行操作,比如各种关闭动作。
下面我们让主线程调用pthread_create函数创建一个新线程,此后新线程就会跑去执行自己的新例程,而主线程则继续执行后续代码。这里我们再打印下新线程的tid。

image-20231213151804994

获取线程ID pthread_self

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

创建线程时通过输出型参数获得。
通过调用pthread_self函数获得。
pthread_self函数的函数原型如下:

pthread_t pthread_self(void);

调用pthread_self函数即可获得当前线程的ID,类似于调用getpid函数获取当前进程的ID。

注意: 用pthread_self函数获得的线程ID与内核的LWP的值是不相等的,pthread_self函数获得的是用户级原生线程库的线程ID,而LWP是内核的轻量级进程ID,它们之间是一对一的关系。

线程等待pthread_join

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

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

参数说明:

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

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

下列代码我们先不管退出码信息,我们将retval设为nullptr

#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 = 500;
    while (true)
    {
        printTid(name, pthread_self());
        sleep(1);
        if (!(cnt--))   break;
    }
    cout << "线程退出啦...." << endl;
    return nullptr;
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, startRoutine, (void*)"thread1");
    sleep(1000);
    pthread_join(tid, nullptr);
    return 0;
}

我们先是给线程发送19号信号让他进行暂停,然后再给他发送18信号让他继续,我们可以发现两次都是同一个线程,因此他们是属于同一个进程的!

我们修改一下代码,让新线程在5s后退出:

#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;
    return nullptr;
}


int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    sleep(10);
    pthread_join(tid,nullptr);
    cout << "main thread join success" << endl;
    sleep(10);
    while (true)
    {
        printTid("main thread", pthread_self());
        sleep(1);
    }

    return 0;
}

监控脚本:

[xiaolu@VM-8-6-centos lesson31]$ while :; do ps -aL | head -1 && ps -aL | grep test; sleep 1; done

image-20231216134151391

我们可以发现新线程在5秒后退出,而此时的线程就剩下一个,但是正常情况下预期应该是两个线程,原因是ps命令在查的时候退出的线程是不给你显示的,所以你只能看到一个线程。但是现在不能证明当前的新线程在退出没有被join的时候就没有内存泄漏。

因此在进程退出的时候要进行join,不然可能会造成内存泄漏的问题!!!

我们来观察一下线程异常的情况(野指针):

#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--))
        {
            int *p=nullptr;
            *p=100;
        }
    }
    cout << "新线程退出啦...." << endl;
    return nullptr;
}


int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    sleep(10);
    pthread_join(tid,nullptr);
    cout << "main thread join success" << endl;
    sleep(10);
    while (true)
    {
        printTid("main thread", pthread_self());
        sleep(1);
    }

    return 0;
}

同样是使用如下的监控脚本辅助我们观察现象:

[xiaolu@VM-8-6-centos lesson31]$ while :; do ps -aL | head -1 && ps -aL | grep test; sleep 1; done

我们发现线程挂了(出现了段错误)!

此时线程异常了,也就是进程异常了,从而会影响进程内其他的线程!!!

image-20231216135157764

我们再反过来了解pthread_join函数的第二参数retval:

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

首先我们知道他是代表错误码的,并且他是void**类型的二级指针。它是一个输出型参数

此线程退出后,我们是通过pthread_join函数获得此线程的退出结果,退出结果是void*类型,可retval是void**类型,我们需要传入一个二级指针。下面演示获得此线程的退出结果的过程,并打印此退出码。

#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;
    return (void*)111;
}



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-20231216140358932

我们先前讲过进程退出时,分为三种情况:

  • 代码跑完,结果正确
  • 代码跑完,结果不正确
  • 异常

在线程退出时,代码跑完,结果不正确和结果正确都可以得到退出码,但是线程异常时并不会出现退出码。那么为什么异常时主线程没有获取新线程退出时的信号呢?

因为线程出异常就不再是线程的问题,而是进程的问题,应该让父进程获取退出码,知道它什么原因退出的。因此线程终止时,只需考虑正常终止

线程终止:

线程终止有三种方法

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

第一种return就不讲了啊

pthread_exit函数

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

retval为线程退出时的退出码信息。

没有返回值

  • 需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

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

总结:

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

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

参数
thread:线程ID
返回值:成功返回0;失败返回错误码

线程是可以取消自己的,取消成功的线程的退出码一般是-1

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

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

线程栈 && pthread_t

pthread_t实际是地址!

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

我们前面也提到,线程之间是共享进程地址空间,对于其他线程来说,如果想看就可以看!

我们之前也提到线程是用用户级线程库,也不是内核级的函数,因此线程也有自己的线程栈

线程库是动态库

当线程需要的时候,将线程库直接加载到共享区就可以使用了

image-20231216143059563

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

*pthread_t究竟是什么呢?*

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

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

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

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

分离线程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,失败返回错误码。

总结分离线程:

  1. 线程分离了,意味着,不在关心这个线程的死活。所以这也相当于线程退出的第4种方式,延后退出。
  2. 立即分离或者延后分离都可以,但是要保证线程活着。
  3. 新线程分离,但是主线程先退出(进程退出),所有线程就都退了。
  4. 一般分离线程,对应的主线程不退出(常驻内存的进程)
    给创建线程的用户返回的是该结构体的起始虚拟地址。所以我们的pthread_t实际上就是用户级线程的控制结构体的起始地址!!!

分离线程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,失败返回错误码。

总结分离线程:

  1. 线程分离了,意味着,不在关心这个线程的死活。所以这也相当于线程退出的第4种方式,延后退出。
  2. 立即分离或者延后分离都可以,但是要保证线程活着。
  3. 新线程分离,但是主线程先退出(进程退出),所有线程就都退了。
  4. 一般分离线程,对应的主线程不退出(常驻内存的进程)
  • 22
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学IT的小卢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值