Bootstrap

Linux多线程【线程控制】

Linux中线程如何理解

线程:是进程内的一个执行分支,线程的执行粒度,要比进程细。

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

在这里插入图片描述

那如何理解线程呢?

  • 在Linux系统下,并没有真正意义上的线程。因为在Linux系统下,线程没有属于自己的数据结构。而windows操作系统是为线程设定了指定的数据结构。而在Linux系统下,线程复用了进程的PCB。也就是说,描述线程和进程的结构体都是task_struct。 而这些PCB都共享同一块进程地址空间,共享同一块页表…以及其他的资源。

在这里插入图片描述

在这里插入图片描述

  • 进程就不仅仅是一个PCB了,而是多个PCB + 当前进程的资源 = 进程。而每一个PCB都是一个执行流,无论是线程还是进程,CPU都不关心。因为CPU只负责调度PCB。而通过一定的技术手段,可以将进程的"资源"以一定的方式分配给不同的task_struct

重新定义线程和进程

  • 在之前的认知中,我们都认为一个进程就是一个PCB + 程序的代码和数据。 但是现在我们要重新认识进程了。当进程内部只有一个执行流的时候, 进程 = PCB + 程序的代码和数据。 当进程内部有多个执行流的时候 ,那么 进程 = 多个PCB + 程序的代码和数据

  • 在CPU的视角中,CPU其实根本不关心当前调用的是进程还是线程,因为它只认PCB,也就是task_struct。所以在linux系统下, PCB <= 其他OS内的PCB。因为当Linux下的进程包含多个执行流的时候,那么多个PCB其实共享了大部分资源,那么此时的PCB就会小于其他OS内的PCB。因为其他的OS,进程和线程都有属于各自的数据结构。

得出结论:

  1. 进程 = 多个PCB(task_struct) + 程序的代码和数据
  2. 进程是承担操作系统分配资源的基本实体
  3. 线程是在进程内部执行流资源。因为它们共享同一块进程地址空间以及其他资源
  4. 线程是CPU调度的基本单元。

  • 进程是程序的一次执行,而线程可以理解为程序中运行的一个片段
  • 由于线程没有独立的地址空间,因此同一个进程的一组线程可以共享访问该进程大部分资源, 这些线程之间的通信也很高效
  • 线程之间的通信简单(共享地址空间和页表信息,因此传参以及全局数据都可以实现通信)。而不同进程之间的通信更为复杂,通常需要调用内核实现。
  • 线程并没有独立的虚拟地址空间只是在进程虚拟地址空间中拥有相对独立的一块空间
  • 不管系统中是否有线程,进程都是拥有资源的独立单位

  • 多进程之间的数据共享比多线程编程复杂,因为线程之间共享地址空间,因此通信更加方便,全局数据以及函数传参都可以实现,而进程间则需要系统调用来完成

  • 多线程的创建,切换,销毁速度快于多进程,因为线程之间共享了进程中的大部分资源,因此共享的数据不需要重新创建或销毁,因此消耗上低于进程,反之也就是速度快于进程

  • 大量的计算使用多进程和多线程都可以实现并行/并发处理,而线程的资源消耗小于多进程,而稳定向较多进程有所不如

  • 多线程没有内存隔离单个线程崩溃会导致整个应用程序的退出,其实不仅仅是内存隔离的问题,还有就是异常针对的是整个进程,因此单个线程的崩溃会导致异常针对进程触发,最终退出整个进程。

  • 一个程序至少有一个进程,一个进程至少有一个线程,这是错的程序是静态的,不涉及进程,进程是程序运行时的实体,是一次程序的运行。

  • 操作系统的最小调度单位是线程

  • 进程是资源的分配单位,所以线程并不拥有系统资源,而是共享使用进程的资源,进程的资源由系统进行分配

  • 任何一个线程都可以创建或撤销另一个线程


  • 多进程里,子进程可复制父进程的所有堆和栈的数据;而线程会与同进程的其他线程共享数据,但拥有自己的栈空间
  • 线程拥有自己的栈空间且共享数据没错,但是资源消耗更小,且便于进程内线程间的资源管理和保护,否则会造成栈混乱。
  • 线程的通信速度更快,切换更快,因为他们在同一地址空间内,且还共享了很多其他的进程资源,比如页表指针这些是不需要切换的
  • 线程使用公共变量/内存时需要使用同步机制,因为他们在同一地址空间内
  • 进程因为每个都有独立的虚拟地址空间,因此通信麻烦,需要调用内核接口实现。而线程间共用同一个虚拟地址空间,通过全局变量以及传参就可实现通信,因此更加灵活方便。

  • 每个线程有自己独立的地址空间,这是错误的,线程只是在进程虚拟地址空间中拥有相对独立的一块空间,但是本质上说用的是同一个地址空间
  • 耗时的操作使用线程,提高应用程序响应,使用多线程可以更加充分利用cpu资源,使任务处理效率更高,进而提高程序响应
  • 对于多核心cpu来说,每个核心都有一套独立的寄存器用于进行程序处理,因此可以同时将多个执行流的信息加载到不同核心上并行运行,充分利用cpu资源提高处理效率
  • 线程包含cpu线程,但是线程只是进程中的一个执行流,执行的是程序中的一个片段代码,多个线程完整整体程序的运行

  • 在linux 中,进程比线程安全的原因是进程之间不会共享数据,错误,进程比线程安全的原因是每个进程有独立的虚拟地址空间有自己独有的数据,具有独立性,不会数据共享这个太过宽泛与片面
  • 进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)
  • 进程——资源分配的最小单位,线程——程序执行的最小单位

重谈地址空间

  • 整体结构如下:
    • 之前没有谈页表,现在我们就要看一下页表是如何进行对虚拟地址到物理地址的转换
    • 前置知识:我们先要知道在C/C++编译后会有虚拟地址(逻辑地址),在一个虚拟地址(这里以32位为例)首先要进行10 + 10 +12 的方式进行分割,前10个比特位是对页目录里的项表进行定位找到二级页表中的页表表项,中间的10个比特位是对二级页表的表项进行定位找到对应的物理地址,然后:后12比特位是对对应的起始地址 + 偏移量就找到了对应的物理地址。

在这里插入图片描述

  • 其实在C语言和C++中取地址只有一个地址,是取的第一个的起始地址(最小的)
  • C++中的空类的大小也是一个字节,也就是一个字节,因为要知道这个空类在哪。
#include<iostream>
using namespace std;

class A
{};

int main()
{
	cout << sizeof(A) << endl;
	return 0;
}

在这里插入图片描述

线程一些周边概念

  • 在CPU内部是有一个cache的缓存里面是存放的热数据

  • cache缓存工作的基本原理是通过将部分主存中的数据复制到更快速但容量较小的缓存中,以便 CPU 在需要时能够更快地获取数据。当 CPU 需要访问数据时,首先检查缓存中是否存在该数据。如果存在(命中),则可以直接从缓存读取,避免了从主存中读取的延迟。如果不存在(未命中),则需要从主存中加载到缓存,并且通常会替换掉缓存中的某些旧数据。

  • 而线程内的切换不需要重新加载cache数据,所以更加轻量化~~

但是地址空间和页表切换并没有太大的消耗。线程切换成本更低的本质原因是因为CPU内部有L1~L3 cache

  • 我们都知道,CPU处理指令是一条一条处理的。但如果每次CPU都去内存读一条指令,那么速度是非常非常慢的。所以CPU内部有个缓冲区。会先把内存中的指令放进CPU内部缓冲区。也就是预读代码,这样CPU就不用频繁的去内存中读取指令。而是直接在内部缓冲区里读,这样子速度是非常快的。而线程切换,cache不会失效。但如果是进程切换,那么cache就会立马失效,只能重新缓冲。所以这才是线程切换更快的本质原因,因为线程切换,CPU内部的缓冲区不用重新缓存。

线程的优点

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

线程的缺点

性能损失

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

健壮性降低

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

缺乏访问控制

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

编程难度提高

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

线程异常

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

线程用途

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

Linux进程VS线程

进程和线程

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

  • 线程是调度的基本单位

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

    • 线程ID
    • 一组寄存器(重要)
    • (重要)
    • errno
    • 信号屏蔽字
    • 调度优先级

进程的多个线程共享

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

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

进程和线程的关系如下图:

在这里插入图片描述

Linux线程控制

  • 内核中没有明确的线程的概念,有轻量级进程的概念

  • 这也就意味着Linux并不能直接给我们提供线程相关的接口,只能提供轻量级进程接口。以库的方式提供给了用户进行使用,那就是pthread线程库(在应用层),为用户提供直接创建线程的接口,也叫原生线程库

创建线程

在这里插入图片描述

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
  • 第一个参数:是一个输出型参数,是线程的tid
  • 第二个参数:线程的属性,一般设置位nullptr
  • 第三个参数:一个函数指针,为线程的执行函数
  • 第四个参数:创建线程成功后,新线程回调线程函数的时候,需要参数,这个参数就是给线程函数传递的

  • 成功返回: 如果线程创建成功,pthread_create 函数将返回 0。
  • 失败返回: 如果线程创建失败,pthread_create 函数将返回一个非零的错误码。这个错误码可以用来判断失败的原因,例如 EAGAIN 表示系统资源不足以创建新线程,EINVAL 表示提供的属性值无效,EPERM 表示调用进程没有足够的权限等。

在这里插入图片描述

测试代码:

makefile:

  • 在编译的时候加上一个-lpthread选项,否则无法编译通过,因为原生线程库并不属于C/C++库,这是一个第三方库
mythread:mythread.cc
	g++ -o $@ $^ -std=c++11 -lp	thread
.PHONY:clean
clean:
	rm -rf mythread

mythread.cc:

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *ThreadRun(void *args)
{
    const char *name = (const char *)args;
    while (true)
    {
        printf("%s,  pid: %d\n", name, getpid());
        sleep(1);
    }

    return (void *)name; // 返回线程名称作为退出值
}

int main()
{
    pthread_t tids[5];
    for (size_t i = 0; i < 5; i++)
    {
        std::string name = "Thread ";
        name += std::to_string(i);
        pthread_create(&tids[i], nullptr, ThreadRun, (void *)name.c_str());
        printf("Thread main, pid: %d\n", getpid());
        sleep(1);
    }
    sleep(5);
    std::cout << "Main thread exiting.\n";
    return 0;
}

然后运行后我们发现。5个线程 + 一个主线程,它们打印出来的进程pid都是一样的

在这里插入图片描述

  • 然后我们再用循环检测命令来查看当前运行的进程
循环检测命令
while :; do ps -axj | head -1 && ps -axj | grep -i mythread | grep -v grep; sleep 1; done;
  • 可以看到在运行的时候始终只有一个进程

在这里插入图片描述

  • 因为线程是进程内部执行的!所以我们无法看到线程

LWP

  • 如果想看线程,我们可以用ps -aL即可查看当前进程下的线程。
while :; do ps -aL | head -1 && ps -aL | grep -i mythread | grep -v grep; sleep 1; done;
  • 我们可以看到这个进程中有6个线程,一个主线程。剩下的5个创建的线程。 我们可以发现它们的PID都是一样的。但是LWP(Light Weight Process)是不一样的! 所以,CPU调度看的是LWP,因为线程是CPU调度的基本单元。如果是根据PID进行调度,那么这么多线程的PID都一样,就会产生歧义。所以CPU调度实际是根据LWP字段调度的

  • LWP是轻量级进程,在Linux下进程是资源分配的基本单位,线程是cpu调度的基本单位,而线程使用进程pcb描述实现,并且同一个进程中的所有pcb共用同一个虚拟地址空间,因此相较于传统进程更加的轻量化

在这里插入图片描述

轻量级进程ID与进程ID之间的区别:

  • Linux下的轻量级进程是一个pcb,每个轻量级进程都有一个自己的轻量级进程ID(pcb中的pid),而同一个程序中的轻量级进程组成线程组,拥有一个共同的线程组ID

此外:我们还可以查看每个线程的CPU占用率

  • 使用top查看的是进程视角
  • 使用top -H查看的是线程视角

验证线程之间共享地址空间

  • 我们只需要在全局上创建一个变量,让新的线程进行修改,然后再进行观察

代码验证:

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

int g_val = 100;

void *ThreadRun(void *args)
{
    const char *name = (const char *)args;
    while (true)
    {
        printf("%s,  pid: %d, g_val: %d, &g_val: 0x%p\n", name, getpid(), g_val, &g_val);
        g_val++;
        sleep(1);
    }

    return (void *)name; // 返回线程名称作为退出值
}

int main()
{
    pthread_t tids[5];

    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRun, (void *)"Thread new");
    while (true)
    {
        printf("Thread main,  pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
        sleep(1);
    }
    sleep(5);
    std::cout << "Main thread exiting.\n";
    return 0;
}

在这里插入图片描述

  • 地址是一样的,但是创建出来的线程修改了值,主线程也可以看到,说明共享了地址空间!

  • 如果其中一个线程出错了也会影响其他线程

  • 比如在代码中出现了除0错误,会直接导致进程退出

在这里插入图片描述

线程中独立的资源

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

  • 线程id
  • 一组寄存器(相当于上下文)
  • 栈(每个线程有独立的栈结构,让线程与线程之间独立)
  • errno
  • 信号屏蔽字
  • 调度优先级

线程等待

int pthread_join(pthread_t thread, void **retval);

在这里插入图片描述

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参 数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

在这里插入图片描述

void *ThreadRun(void *args)
{
    const char *name = (const char *)args;
    int cnt = 5;
    while (cnt)
    {
        printf("%s, cnt: %d\n", name, cnt);
        sleep(1);
        cnt--;
    }
    return (void *)name; // 返回线程名称作为退出值
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRun, (void *)"Thread new");

    void *retval;
    pthread_join(tid, &retval);
    printf("Joined with \"%s\"\n", (char *)retval);
    std::cout << "Main thread exiting.\n";
    return 0;
}

在这里插入图片描述

  • 在学习进程的时候需要考虑进程出异常,而线程为什么不需要考虑异常

    • 因为做不到,子线程出异常,主线程也会出异常,异常问题是进程考虑的

  • 这里的新线程执行完成后直接使用exit退出
void *ThreadRun(void *args)
{
    const char *name = (const char *)args;
    int cnt = 5;
    while (cnt)
    {
        printf("%s, cnt: %d\n", name, cnt);
        sleep(1);
        cnt--;
    }
    exit(11); // 直接退出
    // return (void *)name; // 返回线程名称作为退出值
}
  • exit是用来终止进程的不能用来终止线程

在这里插入图片描述

终止线程

void pthread_exit(void *retval);

在这里插入图片描述

  • 这样可以看到正常终止了
void *ThreadRun(void *args)
{
    const char *name = (const char *)args;
    int cnt = 5;
    while (cnt)
    {
        printf("%s, cnt: %d\n", name, cnt);
        sleep(1);
        cnt--;
    }
    pthread_exit((char *)name);
    // exit(11); // 直接退出
    // return (void *)name; // 返回线程名称作为退出值
}

在这里插入图片描述


取消线程

int pthread_cancel(pthread_t thread);

在这里插入图片描述

void *ThreadRun(void *args)
{
    const char *name = (const char *)args;
    int cnt = 5;
    while (cnt)
    {
        printf("%s, cnt: %d\n", name, cnt);
        sleep(1);
        cnt--;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRun, (void *)"Thread new");

    sleep(1);            // 保证新线程已经启动
    pthread_cancel(tid); // 取消线程

    void *retval;
    pthread_join(tid, &retval);
    printf("Joined with \"%d\"\n", (int*)retval);

    std::cout << "Main thread exiting.\n";
    return 0;
}
  • 这里首先取消了线程,然后join进行等待,发现已经取消了就返回-1,起始是一个宏:PTHREAD_CANCELED
#define PTHREAD_CANCELED ((void *) -1)

在这里插入图片描述

主线程创建出一个新线程,主线程执行pthread_exit(),新的线程会退出吗?
void *PthreadRun(void *args)
{
    int cnt = 0;
    while (true)
    {
        cout << "new Thread, cnt = " << cnt << endl;
        cnt++;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, PthreadRun, nullptr);

    sleep(5);
    pthread_exit(nullptr);
    return 0;
}
  • 我们观察到,主线程执行pthread_exit()后,

在这里插入图片描述

  • Z代表僵尸进程,l代表是一个多线程状态,+代表前台进程

在这里插入图片描述

重谈线程的参数和返回值

  • 线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以传递对象!!
class Request
{
public:
    Request(int start, int end, const string &threadname)
        : start_(start), end_(end), threadname_(threadname)
    {
    }

public:
    int start_;
    int end_;
    string threadname_;
};

class Response
{
public:
    Response(int result, int exitcode) : result_(result), exitcode_(exitcode)
    {
    }

public:
    int result_;   // 计算结果
    int exitcode_; // 计算结果是否可靠
};

void *sumCount(void *args) // 线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以传递对象!!
{
    Request *rq = static_cast<Request *>(args); //  Request *rq = (Request*)args
    Response *rsp = new Response(0, 0);
    for (int i = rq->start_; i <= rq->end_; i++)
    {
        cout << rq->threadname_ << " is runing, caling..., " << i << endl;
        rsp->result_ += i;
        usleep(100000);
    }
    delete rq;
    return rsp;
}

int main()
{
    pthread_t tid;
    Request *rq = new Request(1, 100, "thread 1"); // 计算1到100的和
    pthread_create(&tid, nullptr, sumCount, rq);   // 创建线程

    void *ret;
    pthread_join(tid, &ret);                                                             // 等待线程
    Response *rsp = static_cast<Response *>(ret);                                        // 相当于类型转换
    cout << "rsp->result: " << rsp->result_ << ", exitcode: " << rsp->exitcode_ << endl; // 查看结果

    delete rsp;
    return 0;
}

在这里插入图片描述

  • 目前,我们的原生线程–>pthread库,原生线程库
  • C++11语言本身也已经支持多线程了 vs 原生线程库
#include <thread>

void threadrun()
{
    while (true)
    {
        cout << "I am a new thead for C++" << endl;
        sleep(1);
    }
}

int main()
{
    thread t1(threadrun);

    t1.join();
    return 0;
}
  • 在编译的时候如果不加-lpthread是编译不通过的。

  • 其实C++11的多线程本质,就是对原生线程库接口的封装

线程ID及进程地址空间布局

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

在这里插入图片描述

  • 每一个线程的库级别的tcb的起始地址,叫做线程的tid
  • 除了主线程,所有其他线程的独立栈,都在共享区,具体来说是在pthread,tid指向的用户tcb中,在动态库中统一管理的
pthread_t pthread_self(void);

在这里插入图片描述

  • pthread_t到底是什么类型呢?
    • 取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址
// 转换成十六进制
string toHex(pthread_t tid)
{
    char hex[64];
    snprintf(hex, sizeof(hex), "%p", tid);
    return hex;
}

void *threadRun(void *args)
{
    while (true)
    {
        cout << "Thread id: " << toHex(pthread_self()) << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void *)"Thread 1");

    cout << "main thread create thead done, new thread id : " << toHex(tid) << endl;
    pthread_join(tid, nullptr);
    return 0;
}

在这里插入图片描述


  • 创建一个轻量级子进程,这个接口我们一般不会用,接口很多
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
                 /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );

在这里插入图片描述

  • 这个clone接口被线程库封装了!

  • 在线程库中有线程控制块,而clone被封装成了pthread_create之类的接口,然后通过clone(回到函数,独立栈)进行操作OS
  • 线程的概念是库给我们维护的:线程库要维护线程概念,不用维护线程的执行流
  • 线程库要维护线程概念,线程库注定了要维护多个线程属性集合,线程库也需要管理这些线程,怎么管理?先描述,再组织
  • 用的原生线程库,需要加载到内存中。

创建多线程

#define NUM 10

struct threadData
{
    string threadname;
};

string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%#lx", tid);
    return buffer;
}

void InitThreadData(threadData *td, int number)
{
    td->threadname = "thread-" + to_string(number); // thread-0
}

// 所有的线程,执行的都是这个函数
void *threadRoutine(void *args)
{

    // 每个线程的test_i都是独立的,互不干扰的
    int test_i = 0;
    threadData *td = static_cast<threadData *>(args);


    int i = 0;
    while (i < 10)
    {
        printf("pid: %d, tid:%s, threadname: %s, test_i: %d, &test_i: %p\n", getpid(), toHex(pthread_self()).c_str(), td->threadname.c_str(), test_i, &test_i);
        test_i++;
        sleep(1);
        i++;
    }

    delete td;
    return nullptr;
}
int main()
{
    // 创建多线程
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        // 线程名字
        threadData *td = new threadData; // 这里使用new,在堆上开空间,每个线程独享
        // 初始化线程id
        InitThreadData(td, i);

        // 创建
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid); // 将每个线程使用vector管理起来
        sleep(1);
    }
    sleep(1); // 确保复制成功
    return 0;
}

在这里插入图片描述

  • 还可以对指定thread进行捕捉
#define NUM 10
int *p = NULL;
struct threadData
{
    string threadname;
};

string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%#lx", tid);
    return buffer;
}

void InitThreadData(threadData *td, int number)
{
    td->threadname = "thread-" + to_string(number); // thread-0
}

// 所有的线程,执行的都是这个函数
void *threadRoutine(void *args)
{
    // 每个线程的test_i都是独立的,互不干扰的
    int test_i = 0;
    threadData *td = static_cast<threadData *>(args);
    if(td->threadname == "thread-2") p = &test_i;

    int i = 0;
    while (i < 10)
    {

        printf("pid: %d, tid:%s, threadname: %s, test_i: %d, &test_i: %p\n", getpid(), toHex(pthread_self()).c_str(), td->threadname.c_str(), test_i, &test_i);
        test_i++;
        sleep(1);
        i++;
    }

    delete td;
    return nullptr;
}
int main()
{
    // 创建多线程
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        // 线程名字
        threadData *td = new threadData; // 这里使用new,在堆上开空间,每个线程独享
        // 初始化线程id
        InitThreadData(td, i);

        // 创建
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid); // 将每个线程使用vector管理起来
        // sleep(1);
    }
    sleep(1); // 确保复制成功

    cout << "main thread get a thread local value, val: " << *p << ", &val: " << p << endl;
    return 0;
}

在这里插入图片描述

  • 每一个线程都会有自己的独立的栈结构
  • 其实线程和进程之间,几乎没有秘密
  • 线程的栈上的数据,也是可以被其他线程看到并访问的
  • 全局变量是被所有的线程同时看到并访问的
#define NUM 3
int g_val = 100;
struct threadData
{
    string threadname;
};

string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%#lx", tid);
    return buffer;
}

void InitThreadData(threadData *td, int number)
{
    td->threadname = "thread-" + to_string(number); // thread-0
}

// 所有的线程,执行的都是这个函数
void *threadRoutine(void *args)
{
    // 每个线程的test_i都是独立的,互不干扰的
    threadData *td = static_cast<threadData *>(args);

    int i = 0;
    while (i < 10)
    {
        printf("pid: %d, tid:%s, threadname: %s, g_val: %d, &g_val: %p\n", getpid(), toHex(pthread_self()).c_str(), td->threadname.c_str(), g_val, &g_val);
        g_val++;
        sleep(1);
        i++;
    }

    delete td;
    return nullptr;
}
int main()
{
    // 创建多线程
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        // 线程名字
        threadData *td = new threadData; // 这里使用new,在堆上开空间,每个线程独享
        // 初始化线程id
        InitThreadData(td, i);

        // 创建
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid); // 将每个线程使用vector管理起来
        sleep(1);
    }
    sleep(1); // 确保复制成功
    return 0;
}

在这里插入图片描述

  • 想要一个私有的全局变量就要在全局变量之前加一个__thread,让他变成线程的局部存储,这个修饰符是一个编译选项,只能定义内置类型,不能用来修饰自定义类型
__thread int g_val = 100;

在这里插入图片描述


  • 使用这个可以这样,每个线程都有自己独立的number 和 pid,直接调用即可
#define NUM 3
__thread unsigned int number = 0;
__thread int pid = 0;

struct threadData
{
    string threadname;
};

string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%#lx", tid);
    return buffer;
}

void InitThreadData(threadData *td, int number)
{
    td->threadname = "thread-" + to_string(number); // thread-0
}

// 所有的线程,执行的都是这个函数
void *threadRoutine(void *args)
{
    threadData *td = static_cast<threadData *>(args);
    // if (td->threadname == "thread-2")
    //     p = &test_i;
    string tid = toHex(pthread_self());
    int pid = getpid();

    int i = 0;
    while (i < 10)
    {
        cout << "tid: " << tid << ", pid: " << pid << endl;
        sleep(1);
        i++;
    }

    delete td;
    return nullptr;
}
int main()
{
    // 创建多线程
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        // 线程名字
        threadData *td = new threadData; // 这里使用new,在堆上开空间,每个线程独享
        // 初始化线程id
        InitThreadData(td, i);

        // 创建
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid); // 将每个线程使用vector管理起来
        sleep(1);
    }
    sleep(1); // 确保复制成功
    return 0;
}

在这里插入图片描述

分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

  • 正常主线程分离
int main()
{
    // 创建多线程
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        // 线程名字
        threadData *td = new threadData; // 这里使用new,在堆上开空间,每个线程独享
        // 初始化线程id
        InitThreadData(td, i);

        // 创建
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid); // 将每个线程使用vector管理起来
        sleep(1);
    }
    sleep(1); // 确保复制成功

    // 分离线程
    for (auto i : tids)
    {
        pthread_detach(i);
    }
    return 0;
}

在这里插入图片描述


  • 在分离线程后,再进行等待会报22错误
int main()
{
    // 创建多线程
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        // 线程名字
        threadData *td = new threadData; // 这里使用new,在堆上开空间,每个线程独享
        // 初始化线程id
        InitThreadData(td, i);

        // 创建
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid); // 将每个线程使用vector管理起来
        sleep(1);
    }
    sleep(1); // 确保复制成功

    // 分离线程
    for (auto i : tids)
    {
        pthread_detach(i);
    }
    // pthread_detach后再pthread_join会出错
    for (int i = 0; i < tids.size(); i++)
    {
        int n = pthread_join(tids[i], nullptr);
        printf("n = %d, who = 0x%lx, why: %s\n", n, tids[i], strerror(n));
    }

    return 0;
}

在这里插入图片描述

  • 线程主动分离
// 所有的线程,执行的都是这个函数
void *threadRoutine(void *args)
{
    // 自己把自己分离
    pthread_detach(pthread_self());
    threadData *td = static_cast<threadData *>(args);
    string tid = toHex(pthread_self());
    int pid = getpid();

    int i = 0;
    while (i < 10)
    {
        cout << "tid: " << tid << ", pid: " << pid << endl;
        sleep(1);
        i++;
    }
    delete td;
    return nullptr;
}

在这里插入图片描述

  • 如果主线程再主动等待也是会报错误的~~

在这里插入图片描述

;