前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Linux】:线程概念与控制

【Linux】:线程概念与控制

作者头像
IsLand1314
发布2024-12-20 09:16:47
发布2024-12-20 09:16:47
26500
代码可运行
举报
文章被收录于专栏:学习之路学习之路
运行总次数:0
代码可运行

1. 背景 🚀

还记得我们之前学的进程嘛 ?

  • 进程是操作系统分配资源的基本单位,进程 = 内核数据结构 + 代码和数据,可以将进程可以看作一个运行起来的程序
  • 那么对于线程,我们可以把它看作成一个执行流,执行粒度比进程还要细,是进程内部的一个执行分支

2. Linux 下线程的概念和实现 🎐

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

地址空间是进程访问的资源窗口,下图当中的绿色部分就是线程

线程:在进程内部运行,是CPU调度的基本单位。 进程:承担分配系统资源的基本实体。 我们以前讲的进程内部都是只有一个执行流的进程。

  • Windows系统里有struct tcb结构体描述线程,Linux系统选择复用struct pcb结构体。所以Linux是用进程模拟的线程。
  • Linux中CPU不区分task_struct 是进程还是线程,都看做执行流

CPU看到的: 执行流 <= 进程 注意:Linux中的执行流叫:轻量级进程

🐇 2.2 Linux 下实现
  1. 在 LInux 中,线程在进程“内部”执行,线程在进程的地址空间内运行(为什么?任何执行流要执行,都要有资源!地址空间是进程的资源窗口)
  2. Linux 下的线程,是用进程模拟(复用了 历史代码)实现的

补充知识(LWP)

  • LWP进程线程 之间的一种抽象。在某些操作系统中,LWP 是一种类似于线程的结构,它在操作系统中看起来像一个进程,但又共享进程的一些资源,如内存和文件描述符。LWP 的引入使得操作系统可以提供多任务并发,但不需要为每个 “线程” 创建一个完整的进程。

实现方式(理解):

🔥 在 Linux 统中,线程的实现并不是以传统意义上的“独立的线程”来处理的。Linux 内核并没有将线程作为独立的内核对象来管理,而是通过使用 LWP 来模拟实现的。具体来说,Linux 中的线程模型是基于 线程与进程共享同一内核资源,也就是说,线程实际上是进程的不同执行流。线程的调度和管理是通过 LWP 来实现的。

关键点:

  • Linux 中,线程实际上是 进程的一种特殊形式。每个线程都与其他线程共享进程的资源(如内存空间、文件描述符等),但每个线程都有自己的栈和寄存器。
  • Linux 内核 并不会为每个线程维护一个独立的内核对象,而是将线程当作一个 LWP 来处理。
  • LWP 是一种 轻量级进程,在 Linux 中,每个线程对应于一个 LWP,因此 Linux 中的每个线程都被视为一个独立的进程,但它们共享进程的地址空间。

那么我们现在该如何理解这句话:

❤️‍🔥 注意: Linux 中没有真正意义上的线程,Linux 的线程概念是用 LWP 进行模拟实现的!!!

  • 这句话的意思是 Linux 内核没有将线程作为独立的内核对象 进行实现,而是通过 LWP(轻量级进程) 的机制将每个线程看作是一个轻量级的进程,从而使线程的调度和管理与进程类似,但又保持线程间的共享资源(如内存)特性
  • 具体来说,Linux 中的线程是通过克隆(clone())系统调用实现的,而 clone() 会创建一个新的进程(LWP),但这个新进程会共享父进程的资源。因此,Linux 的线程是依赖于进程模型的,它们共享进程的地址空间,但又具有独立的执行路径和栈

💦 总而言之:Linux 中的线程的确是通过 LWP(轻量级进程)模拟实现的。每个线程在内核中都是一个 LWP,线程共享进程的资源,但内核会为每个线程分配独立的执行上下文(如栈、寄存器等)

🐇 2.3 初识线程创建 -- 理解 LWP 的真实调度
代码语言:javascript
代码运行次数:0
运行
复制
#include <pthread.h>
功能:创建⼀个新的线程
原型:
	int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);

参数:
	thread:返回线程ID
	attr:设置线程的属性,attr为NULL表⽰使⽤默认属性
	start_routine:是个函数地址,线程启动后要执⾏的函数
	arg:传给线程启动函数的参数
	
返回值:成功返回0;失败返回错误码

错误检查:

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

看了线程创建,我们来用代码理解一下:LWP 的真实调度

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>

// 新线程
void *run(void *args)
{
    while(true)
    {
        std::cout << "new thread, pid: " << getpid() << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    std::cout << "我是一个进程: " << getpid() << std::endl;
    pthread_t tid;
    pthread_create(&tid, nullptr, run, (void*)"thread-1");

    // 主线程
    while(true)
    {
        std::cout << "main thread, pid: "<< getpid() << std::endl;
        sleep(1);
    }

    return 0;
}

注意:这里直接编译,会报错,说直接创建线程是未定义的行为

在前面的运行结果中:

  • 因为主次线程里都是死循环打印,结果主次线程都有打印,说明有多执行流,即线程创建成功了
  • 打印出他们的pid,可以看到主次线程的pid都是一样的,因为这两个线程他们都属于同一个进程内部,所以对应的进程pid是一样的

在这里如果我们要查看轻量级进程 ID,需要用到 ps -aL ,而不是 ps -ajx 的那种方式。

这里我们把 pid 和 lwp 都相等的执行流叫做主线程

  • LWP(Light Weight Process) 是什么呢?LWP 得到的是真正的线程ID。之前使用 pthread_self 得到的这个数实际上是一个地址,在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。
  • ps -aL 得到的线程ID,有一个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。

理解:LWP与pthread_create创建的线程之间的关系

  • pthread_create是一个库函数,功能是在用户态创建一个用户线程,而这个线程的运行调度是基于一个轻量级进程实现的。
知识补充:获得当前用户态线程的 id (pthread_self() )
  • 打印出来的 tid 是通过 pthread 库中有函数 pthread_self 得到的,它返回一个 pthreadt类型的变量,指代的是调用 pthread self函数的线程的“ID”
  • 怎么理解这个“ID”呢?这个“ID”是 pthread 库给每个线程定义的进程内唯一标识,是 pthread 库维持的
  • 由于每个进程有自己独立的内存空间,故此“ID”的作用域是进程级而非系统级(内核不认识)。
  • 其实 pthread 库也是通过内核提供的系统调用(例如clone)来创建线程的,而内核会为每个线程创建系统全局唯一的“ID”来唯一标识这个线程

注意:我们这里指的是用户态线程

用户态线程和轻量级进程区别

特性

用户态线程 (User-Level Threads, ULT)

轻量级进程 (LWP)

管理方式

完全由用户态的线程库(如 Pthreads)管理

由操作系统内核管理

调度方式

在用户态进行调度,内核不参与

由操作系统内核进行调度

上下文切换

在用户态完成,不涉及内核上下文切换

内核进行上下文切换,可能涉及内核和用户之间的切换

并发性

只能在单核上执行,因为内核无法并行调度多个线程

支持多核并行,因为 LWP 可以映射为多个内核线程

阻塞

一个线程阻塞时,整个进程会被阻塞

只有当前的 LWP 会阻塞,其他 LWP 可以继续执行

线程数

线程数较大,线程切换成本低

线程数相对较小,但可以在多个核上并行运行

资源共享

线程共享进程的资源,但没有独立的内核线程调度信息

线程共享进程的资源,但每个 LWP 通常对应一个内核线程,或多个 LWP 共享一个内核线程

代码演示如下:

我们把 线程 ID 以十六进制的形式打印出来

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>

// 把线程id转化为 16 进制
std::string toHex(pthread_t tid){
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%lx", tid);
    return buffer;
}


void *routine(void *args){
    std::string name = static_cast<const char*>(args);
    while(true){
        std::cout << "我是新线程, 我的名字是" << name << ", my tid is : " << toHex(pthread_self()) << std::endl;
        sleep(1);
    }
    return 0;
}

int main(){
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, (void*)"thread-1");
    printf("new thread id: 0x0%lx\n", tid);
    while(true){
        std::cout << "我是 main线程..." << std::endl;
        sleep(1);
    }
    return 0;
}

注意:

  1. 新线程 和 main 线程谁先运行 -> 不确定
  2. 那么为什么没有换行输出呢?? 原因如下:

1)多线程输出交错:

  • std::cout 在多线程程序中并不是线程安全的。即使是同一个线程的输出,std::cout 也可能因为缓冲机制而导致输出顺序不一致。两个线程同时操作输出流时,可能会发生竟争导致输出的内容交错在一起。
  • 由于 std:.cout 是缓冲输出的,可能在你看到 "我是 main线程…" 时,实际上这个文本已经被写入了缓,冲区,还没有真正被输出到屏幕上,然后,新线程输出的内容(例如“我是声线程,我的名字是thread-1")也可能同时进入缓冲区,并且被混合在一起打印。

2)线程的调度和切换:

  • 线程的执行顺序不一定是按照创建的顺序进行的。操作系统的线程调度器决定了哪个线程什么时候运行。这意味着min线程和romntim:线程的执行是交替进行的,有时 main线程的输出还没完全打印出来,另一个线程就已经开始打印了。
  • 由于你没有显式地控制线程的执行顺序或者输出顺序,可能会看到不同的交错输出。

3. 线程的特性及用途 📚

🦄 3.1 线程的优点
  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
    • 最主要的区别是:线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。
      • 这两种O下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
    • 另外一个隐藏的损耗是:上下文的切换会扰乱处理器的缓存机制。
      • 简单的说,一旦去切换上文,处理器中所有已经缓存的内存地址一瞬间都作废了
    • 还有一个显著的区别是:当你改变虚拟内存空间的时候,处理的页表缓冲 TLB(快表)会被全部刷新,这将导致内存的访问在段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件 cache
  • 线程占用的资源要比进程少很多
  • 线程能充分利用多处理器的可并行数量
  • 在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作
🦄 3.2 线程的缺点

a. 性能损失

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

b. 健壮性降低

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

c. 缺乏访问控制

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

d. 编程难度提高

  • 编写与调试一个多线程程序比单线程程序困难得多
🦄 3.3 线程的异常
  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>

// 把地址转化为 16 进制
std::string toHex(pthread_t tid){
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%lx", tid);
    return buffer;
}



// 多个线程执行一个函数:routine 被重入了
// 一切皆文件:向显示器打印就是向文件写入,所以显示器文件就相当于被线程共享的公共资源
void *routine1(void *args){
    std::string name = static_cast<const char*>(args);
    while(true){
        // 3. 不加保护的情况下,显示器文件就是共享资源
        std::cout << "我是新线程, 我的名字是" << name << ", my tid is : " << toHex(pthread_self()) << std::endl;
        sleep(1);
    }
    return 0;
}


void *routine2 (void *args){
    std::string name = static_cast<const char*>(args);
    while(true){
        // 3. 不加保护的情况下,显示器文件就是共享资源
        std::cout << "我是新线程, 我的名字是" << name << ", my tid is : " << toHex(pthread_self()) << std::endl;
        sleep(1);
        // 6. 线程一旦出现异常,可能会导致其他线程全部崩溃
        // 6.1 异常的本质: 信号
        int *p = nullptr;
        *p = 100;
    }
    return 0;
}

int main(){
    // 1. 新线程 和 main 线程谁先运行 -> 不确定
    // 2. 线程创建出来,要对进程的时间片进行瓜分

    pthread_t tid1;
    pthread_create(&tid1, nullptr, routine1, (void*)"thread-1");

    pthread_t tid2;
    pthread_create(&tid2, nullptr, routine2, (void*)"thread-2");


    printf("new thread id: 0x0%lx\n", tid1);
    printf("new thread id: 0x0%lx\n", tid2);


    while(true){
        std::cout << "我是 main线程..." << std::endl;
        sleep(1);
    }
    return 0;
}

运行结果如下:

  • 线程一旦出现异常,可能会导致其他线程全部崩溃,整个进程就崩溃了,包括主线程。因为信号是发给进程的,不是发给线程的。
  • 异常的本质: 信号

如果新线程异常了,

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

4. 重谈进程和线程 🖊

🦌 4.1 进程和线程
  • 进程是资源分配的基本单位, 因此:线程并不拥有系统资源,而是共享使用进程的资源,进程的资源由系统进行分配
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据:
    • 线程ID
    • 一组寄存器(重要)
    • 栈(重要)(线程运行的时候,会形成各种临时变量,临时变量会被每个线程保存在自己的栈区),注意: 多个线程堆区共用
    • errno
    • 信号屏蔽字
    • 调度优先级

虽然每个线程在进程虚拟地址空间中会分配拥有相对独立的栈空间,而并不是共享栈空间,这样会导致运行时栈混乱

🦌 4.2 进程的多个线程共享

同一地址空间,Text Segment、Data Segment都是共享的。

如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

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

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

如何看待之前学习的单进程?具有一个线程执行流的进程!

🐻 共享进程内函数
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <thread>

// 把地址转化为 16 进制
std::string toHex(pthread_t tid){
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%lx", tid);
    return buffer;
}


void *routine(void *args){
    std::string name = static_cast<const char*>(args);
    while(true){
        std::cout << "我是新线程, 我的名字是" << name << ", my tid is : " << toHex(pthread_self()) << std::endl;
        sleep(1);
    }
    return 0;
}

int main(){
    // 1. 新线程 和 main 线程谁先运行 -> 不确定
    // 2. 线程创建出来,要对进程的时间片进行瓜分
    
    // 3. 多个线程
    pthread_t tid1;
    pthread_create(&tid1, nullptr, routine, (void*)"thread-1");
    printf("new thread id: 0x0%lx\n", tid1);

    pthread_t tid2;
    pthread_create(&tid2, nullptr, routine, (void*)"thread-2");
    printf("new thread id: 0x0%lx\n", tid2);
    
    pthread_t tid3;
    pthread_create(&tid3, nullptr, routine, (void*)"thread-3");
    printf("new thread id: 0x0%lx\n", tid3);


    while(true){
        std::cout << "我是 main线程..." << std::endl;
        sleep(1);
    }
    return 0;
}

// 输出结果如下:
island@VM-8-10-ubuntu:~/code$ ./code
new thread id: 0x07ff7a62ea700
new thread id: 0x07ff7a5ae9700
new thread id: 0x07ff7a52e8700
我是 main线程...
我是新线程, 我的名字是thread-1, my tid is : 0x7ff7a62ea700
我是新线程, 我的名字是thread-3, my tid is : 0x7ff7a52e8700
我是新线程, 我的名字是thread-2, my tid is : 0x7ff7a5ae9700
..........

结论:

  • 线程创建出来,要对进程的时间片进行瓜分
  • 多个线程执行一个函数:routine 被重入了
  • 一切皆文件:向显示器打印就是向文件写入,所以显示器文件就相当于被线程共享的公共资源
  • 不加保护的情况下,显示器文件就是共享资源
🐻 共享全局变量

结论:全局变量在线程内都是共享的

  • 在多线程代码中,想要多个线程看到同一份资源只需要用全局变量就可以了
🐻 __thread

刚刚上面演示了 一个全局变量被多线程共享,那么如果我们想让多个线程各自私有这一个变量,该怎么做呢? --> g++有一个编译选项 __thread

  • 用__thread修饰这个全局变量即可。运行后,主线程和新线程gval的地址也不一样了。这种情况叫线程的局部存储,原始代码里只看到一个gval,但是他们用的是各自的gval
  • 这种情况只在Linux中有效。__thread只能用来修饰内置类型。
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>


// 线程局部存储
// 编译型关键字,给每个线程来一份
// 虽然用的是同一份值和变量名,但是编译的时候把它地址编程不同
// 此时就叫做线程局部存储
// 注意:__thread 只能修饰内置类型
__thread int gval = 100; // 此时主线程和新线程看到的地址不同


// 把地址转化为 16 进制
std::string toHex(pthread_t tid){
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%lx", tid);
    return buffer;
}


void *start(void *args)
{
    std::string name = static_cast<const char*>(args);
    sleep(1);
    while(true){
        printf("I am a new thread, name: %s, gval: %d, &gval: %p\n", name.c_str(), gval, &gval);
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start, (void*)"thread-1");
    std::cout << "I am a new thread, name: main " << toHex(pthread_self()) 
        << ", New thread id: " << toHex(tid)  << std::endl;

    while(true) {
        printf("main thread, gval: %d, &gval: %p\n", gval, &gval);
        gval += 10;
        sleep(1);
    }

    pthread_join(tid, nullptr);
    return 0;
}

运行结果如下:

🐻 不同线程通过地址访问其他线程栈

在多线程中什么都是共享的,那么这个有什么用呢?

  • 可以使得不同线程通过地址访问其他线程的栈
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>

int *addr = nullptr;

void *start1(void *args)
{
    std::string name = static_cast<const char*>(args);
    int a = 100;
    addr = &a;
    while(true){
        std::cout << name << " local val a: " << a << std::endl;
        sleep(1);
    }
}

void *start2(void *args)
{
    std::string name = static_cast<const char*>(args);
    while(true){
        if(addr != nullptr)
            std::cout << name << " mod val a: " << (*addr)++ << std::endl;
        sleep(1);
    }
}

int main()
{
    // 不同线程通过地址可以访问其他线程的栈
    pthread_t tid1, tid2;
    pthread_create(&tid1, nullptr, start1, (void*)"thread-1");
    pthread_create(&tid2, nullptr, start2, (void*)"thread-2");

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);

    return 0;
}

// 输出
thread-1 local val a: 100
thread-2 mod val a: 100
thread-2 mod val a: 101
thread-1 local val a: 102
thread-1 local val a: 102
thread-2 mod val a: 102
thread-1 local val a: 103
thread-2 mod val a: 103
  • 尽管每个线程有独立的栈空间,但在此代码中,addr 是一个全局变量,它指向了线程 1 的栈上局部变量 a 的地址。通过这个全局指针,线程 2 可以访问和修改线程 1 的栈上局部变量 a。
  • 这种做法在技术上是可能的,因为 a 的地址被赋值给了 addr,并且 addr 是一个全局指针,因此任何线程都可以通过 addr 访问 a。
  • 这意味着,尽管 a 是线程 1 的局部变量,线程 2 通过全局变量 addr 可以访问并修改线程 1 的局部变量 a

为什么可以访问另一个线程的栈?

  • 栈和局部变量的存储方式:每个线程的栈空间通常是在内存中的不同位置分配的。当 a 被定义为线程 1 的局部变量时,它的内存地址是分配在该线程的栈上。然后,addr 被设置为 &a,即 addr 指向线程 1 的栈上的地址。
  • 全局指针共享:由于 addr 是全局变量,它可以被其他线程访问。因此,线程 2 可以通过 addr 获取线程 1 的栈上变量 a 的地址,并且通过该地址修改 a 的值。

注意:通过线程间共享局部栈变量的地址是非常危险的,属于未定义行为。线程 1 的栈空间在线程 1 结束时会被清理,而线程 2 持有的 addr 可能指向已被释放的内存。这样做可能会导致程序崩溃或访问无效内存

5. 线程控制 🧃

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

🔥 前面已经简单介绍了pthread_create的使用。在创建完成后,主线程会继续向下执行代码,新线程会去执行参数3所指向的函数。此时执行流就一分为二了

这里我们再来一个例子,用 线程传递参数 --> 结构体,来验证一个结论--> 线程传参问题

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>

class ThreadData
{
public:
    ThreadData(const std::string &name, int a, int b): _name(name), _a(a), _b(b)
    {}
    int Excute(){return  _a + _b;}

    std::string Name(){return _name;}
    ~ThreadData()
    {}

private:
    std::string _name;
    int _a, _b;

};

void *routine(void *args){
    ThreadData *td = static_cast<ThreadData *> (args);
    while(true){
        std::cout << "我是新线程, 我的名字是" << td->Name()  << std::endl;
        std::cout << "task result is : " << td->Excute() << std::endl;
        sleep(1);
        // break; // 结束当前线程死循环
    }
    return 0;
}

int main()
{
    pthread_t tid;
    ThreadData *td = new ThreadData("thread-1", 10, 20);
    pthread_create(&tid, nullptr, routine, td);

    printf("new thread id: 0x0%lx\n", tid);

    while(true){
        std::cout << "我是 main线程..." << std::endl;
        sleep(1);
    }
    
    return 0;
}

结论:线程函数传参,可以传任意类型,一定要记住还可以传类对象的地址。 有了这个,就意味着可以给线程传递多个参数,甚至方法了。

对于 pthread_create 传参的问题,有两种方法,如下:

  • 上面的 td 对象是在主线程的栈上的,新线程访问了主线程栈上的临时变量,我们不推荐这种做法。
  • 因为如果main函数有第二个对象,他们在读取时没有影响,但其中一个对象在修改时,另一个也会跟着修改

因此我们更推荐之前代码中的做法:

  • 我们建议在堆上申请一段空间,未来需要第二个对象时,再重新new一个对象,这样多线程就不会互相干扰了。
🐳 5.3 线程等待

线程创建之后,也是需要被等待和回收的

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内
  • 创建新的线程不会复用刚才退出线程的地址空间
代码语言:javascript
代码运行次数:0
运行
复制
功能:等待线程结束
原型:
int pthread_join(pthread_t thread, void **value_ptr);

参数:
thread:线程ID
value_ptr:它指向⼀个指针,后者指向线程的返回值

参数2的类型是void**,用来接收新线程函数的返回值,因为新线程函数的返回值类型是void*。
未来要拿到新线程的返回值void*,放到void* retval中时,这里的参数就得传&retval。

返回值:成功返回0;失败返回错误码

💢 调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。thread 线程以不同的方法终止,通过 pthread join 得到的终止状态是不同的,总结如下:(这个大家可以在先看了进程终止之后再看这里)

  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 参数

代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>

void *routine(void *args){
    int cnt = 3;
    while(cnt){
        std::cout << "new threa run ..., cnt: " << cnt-- << std::endl;
        sleep(1);
    }
    return 0;
}

int main(){
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, (void*)"thread-1");

    std::cout << "main thread join begin..." << std::endl;
    int n =pthread_join(tid, nullptr);

    while(n == 0){
        std::cout << "main thread join success..." << std::endl;
        sleep(1);
    }
    
    return 0;
}

// 输出:
main thread join begin...
new threa run ..., cnt: 3
new threa run ..., cnt: 2
new threa run ..., cnt: 1
main thread join success...
main thread join success...
🐳 5.4 线程返回值
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>

void *routine(void *args){
    int cnt = 3;
    while(cnt){
        std::cout << "new threa run ..., cnt: " << cnt-- << std::endl;
        sleep(1);
    }
    return (void*)1314;
}

int main(){
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, (void*)"thread-1");

    std::cout << "main thread join begin..." << std::endl;

    void *ret = nullptr; // 开辟了空间的
    int n = pthread_join(tid, &ret); // 传递该空间地址 
    
    std::cout << "join success! ret: " << (long long int) ret <<std::endl; // 等待新线程退出后,结束等待

    return 0;
}
  • 如果新线程返回值不是 nullptr 或者 0,而是别的退出信息时。主线程可以在join时拿到退出信息,如上图,定义了一个指针 ret,这个指针是开辟了空间的,把空间的地址传过去,就能拿到退出信息了。

返回值还可以是类对象的地址,主线程接收时用对应类类型对象接收即可

注意:在这里只考虑正确的返回值,不考虑异常,因为异常时整个程序就挂掉了。

🐳 5.5 创建多线程 -- 练习&巩固
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>

class ThreadData
{
public:
    ThreadData()
    {}
    void Init(const std::string &name, int a, int b){
        _name = name;
        _a = a;
        _b = b;
    }
    
    void Excute(){_result =  _a + _b;}

    int Result(){return _result;}

    std::string Name(){return _name;}
    void SetId(pthread_t tid) {_tid = tid;}
    pthread_t Id() {return _tid;}
    int A() {return _a;}
    int B() {return _b;}
    ~ThreadData()
    {}

private:
    std::string _name;
    int _a, _b;
    int _result; // 返回结果
    pthread_t _tid;
};

int gval = 100;

std::string toHex(pthread_t tid){
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%lx", tid);
    return buffer;
}

void *routine(void *args){
    ThreadData *td = static_cast<ThreadData *> (args);
    while(true){
        std::cout << "我是新线程, 我的名字是" << td->Name() << ", my tid is : " << toHex(pthread_self()) << ", 全局变量(会修改): " << gval << std::endl;
        gval++;
        td->Excute();
        sleep(1);
        break; // 结束当前线程死循环
    }
    return td;
}


// 现在有一批数据,每一个数据的结果都让线程进行运算并且进行汇总
#define NUM 10
int main()
{
    ThreadData td[NUM];
    // 1. 准备好我们要价格处理的数据
    for(int i = 0; i < NUM; i++){
        char id[64];
        snprintf(id, sizeof(id), "thread-%d", i);
        td[i].Init(id, i * 10, i * 20);
    }

    // 2. 创建多线程
    for(int i = 0; i < NUM; i++){
        pthread_t id;
        pthread_create(&id, nullptr, routine, &td[i]);
        td[i].SetId(id);
    }

    // 3. 等待多个线程
    for(int i = 0; i < NUM; i++)
    {
        pthread_join(td[i].Id(), nullptr);
    }

    // 4. 汇总处理结果
    for(int i = 0; i < NUM; i++)
    {
        printf("td[%d]: %d + %d = %d[%ld]\n", i, td[i].A(), td[i].B(), td[i].Result(), td[i].Id());
    }

    return 0;
}

运行结果如下:

🐳 5.6 线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_exit终止自己。
  3. 一个线程可以调用 pthread_cancel 终止同一进程中的另一个线程。

pthread exit函数

代码语言:javascript
代码运行次数:0
运行
复制
功能:线程终⽌
原型:
void pthread_exit(void *value_ptr);

参数:
value_ptr:value_ptr不要指向⼀个局部变量。

返回值:
⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)
  • 需要注意,pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了

pthread cancel函数

代码语言:javascript
代码运行次数:0
运行
复制
功能:取消⼀个执⾏中的线程
原型:
int pthread_cancel(pthread_t thread);

参数:
thread:线程ID

返回值:成功返回0;失败返回错误码
  • 注意:主线程调用 pthread_cancel 取消新线程。取消一个线程的前提是线程得存在。
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>

void *start(void *args)
{
    std::string name = static_cast<const char*>(args);
    while(true){
        std::cout << "I am a new thread" << std::endl;
        sleep(1);
        //break;
    }
    // return 0; // 10. 新线程 return 表示该线程退出
    // exit(1); // 任何地方内部调用 exit,表示进程退出!! 尽管是在线程内部
    //pthread_exit((void*)10); // 第二种退出方法,和 return 等价
}


int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start, (void*)"thread-1");

    sleep(3);
    
    pthread_cancel(tid); // 第三种退出方式
    std::cout << "取消线程: " << tid << std::endl;
   
    sleep(3);

    void *ret = nullptr;
    //一个线程被取消之后,ret = -1,线程的退出码是 PTHREAD_CANCELD
    int n = pthread_join(tid, &ret);
    std::cout << "new thread exit code: " << (long long int)ret << ", n: " << n << std::endl;

    return 0; // 10. 主线程return: 表示进程结束
}

运行结果如下:

  • 线程取消一个就join一个。由上图可知,线程被取消后,线程的退出结果是 -1
  • -1对应pthread库中的一个宏 #define PTHREAD_CANCELD ((void*)-1)
🐳 5.7 线程分离
  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
  • 如果一个线程被分离,线程的工作状态就是分离状态,不需要被join,但依旧属于进程内部
代码语言:javascript
代码运行次数:0
运行
复制
int pthread_detach(pthread_t thread);

可以是线程组内其他线程对⽬标线程进⾏分离,也可以是线程自己分离:

int pthread_detach(pthread_self());
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <pthread.h>

void *start(void *args)
{
    pthread_detach(pthread_self()); // 线程自己把自己分离
    std::string name = static_cast<const char*>(args);
    while(true){
        std::cout << "I am a new thread" << std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start, (void*)"thread-1");
// 除了可以让新线程自己分离,也可以由主线程进行分离
    //pthread_detach(tid); // 主线程对指定线程做分离

    sleep(3);
    void *ret = nullptr;
    int n = pthread_join(tid, &ret);
    std::cout << "new thread exit code: " << (long long int)ret << ", n: " << n << std::endl;

    return 0; 
}

运行结果如下:

原因:

  • 线程自己把线程自己分离,主线程进入join 失败了然后退出,因此没有阻塞
  • 现在让主线程把线程分离,此时主线程也会退出,说明线程一旦被分离,就不能 Join 了
  • 主线程不会卡在 Join,而是会继续往后走,主线程结束了,整个进程就结束了。这样的话,假如我们还有其他线程,可能它们还没起来就死亡了
  • 所以分离线程后,主线程就可以做自己的事了,不用管新线程,即使新线程分离,只要分离的线程异常了,还是会影响整个进程。

注意:在多执行流情况下,主执行流是最后退出的

6. 线程ID 和 进程地址空间布局 🎈

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

pthread_t 到底是什么类型呢?取决于实现。对于Linux⽬前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

  • Linux只维护轻量级进程,linux中的pcb里与执行流相关的属性都是轻量级进程的属性,所有的属性都是围绕lwp展开的。
  • 我们在用户层的概念是线程,要的是线程的id,与线程相关的内容在Linux中是没有的,它没有维护。所以这部分属性由库来进行维护。
  • 为了更好的管理线程,创建线程时,库会为我们的每一个线程申请一个内存块(描述线程的相关结构体字段属性)。
  • 未来要找一个线程的所有属性,只要找到线程控制块的地址即可。所以pthread_t id就是一个地址。
  • pthread_t类型的线程ID,本质就是线程属性集合的起始虚拟地址 ---- 在pthread库中维护。

  • 动态库被运行时加载,动态库没被加载前在磁盘中。
  • pthread库本质是一个文件。
  • 我们刚刚写的可执行程序,它也是个文件,所以他也在磁盘中。可执行程序内部用线程库来创建多线程。
  • 程序运行时,会变成一个进程,加载到内存中,内存中就有该进程的代码和数据。
  • 创建线程时,要先把库加载到内存中,然后再映射到该进程的地址空间才能用。
  • 映射要映射到堆栈之间的共享区。
  • 如果有多个多线程进程,它只需要把共享区的代码,经过页表映射到已经加载到内存的库,此时多个进程就可以使用同一个库里的方法来创建线程。

7. thread VS pthread.h -- 小结📖

这里我想要说一下的就是关于 头文件 <thread> 和 头文件 <pthread.h>

在 C++ 中,<thread> 和 pthread.h 都用于线程编程,但它们分别属于不同的库和标准,并有不同的特性。下面我将详细介绍这两个头文件以及它们的区别。

🥑 a. <thread> 头文件 (C++11 引入)

<thread> 是 C++11 标准引入的头文件,提供了对多线程编程的标准支持。使用这个头文件,C++ 程序可以方便地创建和管理线程。它提供了一个高级的线程抽象,封装了底层的线程管理,使得多线程编程变得更简洁。

主要特点:

  • 面向对象的接口:使用 std::thread 类创建和管理线程。
  • 跨平台支持:std::thread 是 C++ 标准库的一部分,能够在支持 C++11 的编译器和操作系统上工作,且跨平台性较好。
  • 简化的线程操作:自动管理线程的创建、启动、同步等操作。

示例代码:

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <thread>

void print_hello() {
    std::cout << "Hello from the thread!" << std::endl;
}

int main() {
    // 创建一个新线程
    std::thread t(print_hello);

    // 等待线程执行完毕
    t.join();  // join() 会阻塞当前线程,直到新线程完成

    std::cout << "Main thread finished." << std::endl;
    return 0;
}

// 输出:
Hello from the thread!
Main thread finished.

主要功能:

  • std::thread:创建线程对象,并将线程函数传递给它。
  • join():等待线程完成。
  • detach():使线程在后台执行,不能再使用 join() 等待它完成。
  • 异常处理:线程如果抛出异常,它会传播到 join() 或 detach(),可以通过异常捕获来处理。
🥑 b. pthread.h 头文件 (POSIX 标准)

pthread.h 是 POSIX 标准中定义的线程库,通常用于类 Unix 操作系统(如 Linux 和 macOS)。这个库提供了更底层的线程控制,使用起来相对复杂,需要更多的手动管理,但也提供了更灵活的功能。

主要特点:

  • 底层的线程管理:提供更细粒度的控制,包括线程创建、同步、锁机制等。
  • 跨平台的有限性:主要用于类 Unix 操作系统(Linux、macOS 等)。Windows 不直接支持 pthread.h,但有类似的实现(如 pthreads-win32)。
  • 适用场景:适用于需要精细控制线程行为的场景。

示例代码:

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <pthread.h>

void* print_hello(void* arg) {
    std::cout << "Hello from the thread!" << std::endl;
    return nullptr;
}

int main() {
    pthread_t thread;

    // 创建一个新线程
    pthread_create(&thread, nullptr, print_hello, nullptr);

    // 等待线程执行完毕
    pthread_join(thread, nullptr);  // 阻塞主线程,直到新线程结束

    std::cout << "Main thread finished." << std::endl;
    return 0;
}

输出:
Hello from the thread!
Main thread finished.

主要功能:

  • pthread_create():用于创建线程并启动执行。
  • pthread_join():等待线程完成,类似于 std::thread::join()。
  • pthread_detach():分离线程,让其独立运行,不需要等待。
  • 同步机制:提供互斥锁(pthread_mutex_t)、条件变量(pthread_cond_t)等同步工具。
🥑 c. 对比总结

特性

<thread> 头文件 (C++11)

<pthread.h> 头文件 (POSIX)

标准

C++11 标准

POSIX 标准(主要在类 Unix 系统上)

接口层次

高级抽象,面向对象的接口

低级抽象,面向过程的接口

跨平台支持

支持 C++11 标准的所有平台

主要支持类 Unix 操作系统

线程创建

std::thread

pthread_create()

线程同步

join(), detach()

pthread_join(), pthread_detach()

同步工具

通过 C++ 标准库的 <mutex> 和 <condition_variable> 提供

提供互斥锁(pthread_mutex_t)和条件变量(pthread_cond_t)等

异常处理

支持线程抛出异常,并能传播至主线程

需要手动管理异常处理

使用难度

更简洁,容易理解

更复杂,更多控制,适合低级编程

🥑 d. 使用场景

使用 <thread>:

  • 当你使用 C++11 或更高版本,并希望保持代码简单且跨平台时,<thread> 是首选。
  • 适合大多数常见的多线程编程任务。

使用 pthread.h:

  • 当你需要对线程进行更多控制,或者在 POSIX 环境下工作时,pthread.h 提供了更多的底层功能。
  • 适合对线程行为要求非常精细的程序,或者需要在类 Unix 操作系统上运行的应用程序。

总的来说

  • <thread> 是 C++11 标准库的一部分,提供了简洁且高效的线程抽象,适合大多数多线程应用。
  • pthread.h 是 POSIX 标准库的一部分,适用于更低级的线程控制,适合在类 Unix 操作系统上进行多线程编程。

那么这里我们就把线程的概念以及控制就讲完啦,后面我们就要开始线程互斥与等待啦,敬请期待!!!

【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 背景 🚀
  • 2. Linux 下线程的概念和实现 🎐
    • 🐇 2.1 基本概念
    • 🐇 2.2 Linux 下实现
    • 🐇 2.3 初识线程创建 -- 理解 LWP 的真实调度
    • 知识补充:获得当前用户态线程的 id (pthread_self() )
  • 3. 线程的特性及用途 📚
    • 🦄 3.1 线程的优点
    • 🦄 3.2 线程的缺点
    • 🦄 3.3 线程的异常
    • 🦄 3.4 线程的用途
  • 4. 重谈进程和线程 🖊
    • 🦌 4.1 进程和线程
    • 🦌 4.2 进程的多个线程共享
      • 🐻 共享进程内函数
      • 🐻 共享全局变量
      • 🐻 __thread
      • 🐻 不同线程通过地址访问其他线程栈
  • 5. 线程控制 🧃
    • 🐳 5.1 POSIX 线程库
    • 🐳 5.2 创建线程 --> 线程函数传参
    • 🐳 5.3 线程等待
    • 🐳 5.4 线程返回值
    • 🐳 5.5 创建多线程 -- 练习&巩固
    • 🐳 5.6 线程终止
    • 🐳 5.7 线程分离
  • 6. 线程ID 和 进程地址空间布局 🎈
  • 7. thread VS pthread.h -- 小结📖
    • 🥑 a. <thread> 头文件 (C++11 引入)
    • 🥑 b. pthread.h 头文件 (POSIX 标准)
    • 🥑 c. 对比总结
    • 🥑 d. 使用场景
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档