Linux线程:管理与控制

一、引言

随着计算机硬件技术的飞速发展,尤其是多核CPU的普及,多线程编程已成为充分利用系统资源、提高程序并发性和响应速度的关键技术。

多线程编程允许一个程序中同时运行多个线程,每个线程可以独立地执行不同的任务。这种并行处理的方式能够显著减少程序的执行时间,提高程序的运行效率。同时,多线程编程还可以提升用户体验,因为多个线程可以同时处理不同的用户请求,使得系统能够更快地响应用户的操作。

在Linux系统中,线程得到了强大的支持。Linux内核为线程提供了丰富的功能和灵活的机制,使得开发者可以轻松地创建、管理和控制线程。Linux系统的线程模型基于POSIX线程(Pthreads)标准,该标准定义了一套用于创建、同步和管理线程的API,使得开发者可以跨平台地使用这些API来编写多线程程序。

Linux系统的线程具有以下几个特点:

  1. 线程轻量级:Linux线程的实现基于轻量级进程(LWP),相比于传统的进程,线程在创建和销毁时的开销更小,因此更适合用于实现高并发的应用程序。
  2. 共享内存空间:线程之间共享同一进程的地址空间,这使得线程之间的数据共享和通信变得非常简单和高效。
  3. 线程间通信与同步:Linux系统提供了多种线程间通信和同步的机制,如互斥锁、条件变量、信号量等,这些机制可以有效地协调线程之间的执行,确保程序的正确性和稳定性。
  4. 可移植性:Linux系统的线程模型基于POSIX标准,这使得Linux线程程序具有很好的可移植性,可以在不同的操作系统和平台上运行。

二、理解线程

1、线程的定义

线程是操作系统能够进行调度的最小单位,是进程内的一个执行单元。它负责在程序里独立执行一个控制流(线程流),拥有独立的执行栈和程序计数器(PC),用于保存线程上下文信息。线程本身不拥有系统级的独立资源(如独立的内存空间、文件描述符表等),而是与同属一个进程的其他线程共享进程所拥有的全部资源。

线程拥有一些运行中必不可少的资源,如程序计数器、一组寄存器和栈,以支持其独立的执行路径。

在Linux中,线程是通过在相同的地址空间内创建多个task_struct结构体来实现的,这些task_struct结构体表示了线程的状态和相关信息。上文中提到,尽管线程之间共享进程的地址空间,但每个线程都拥有自己独立的执行栈、程序计数器和线程ID,以确保线程执行的独立性和可调度性。进程地址空间与线程task_struct的关系如下图所示:

在这里插入图片描述

在Linux系统中,每个进程都有其自己的地址空间,这个地址空间是虚拟的,由内核管理。内核使用mm_struct结构体来表示进程的地址空间。

在Linux和其他大多数现代操作系统中,一个进程(包括其所有线程)所能访问的资源都是通过其地址空间来访问的。地址空间是一个虚拟的内存区域,它包含了进程需要的所有信息,如代码、数据、堆和栈等。进程是操作系统进行资源分配和调度的基本单位。每个进程都有其独立的地址空间、页表、代码、数据和至少一个执行流(主线程)。

而线程作为进程的一部分,共享同一个进程的地址空间和其他资源,在进程的虚拟地址空间内运行。这意味着线程可以直接访问进程的数据段、代码段和堆栈段,而无需进行任何特殊的系统调用或进程间通信。然而,线程也保持了独立性,因为它们拥有自己的task_struct和执行栈,使得操作系统能够单独调度每个线程的执行。

在Linux中,每个进程至少有一个线程,这个线程通常被称为主线程或初始线程。当一个新的进程被创建时,它会自动包含一个执行线程。

总结下来就是,进程是操作系统进行资源分配和调度的基本单位。线程是操作系统能够进行调度的最小单位,是进程内的一个执行单元。

那么我们说,进程是资源分配的最小单位,线程是CPU调度的最小单位

线程是进程的一个执行单元,它们共享进程的地址空间,包括上述的所有区域(除了栈之外,栈是每个线程私有的)。这种共享使得线程之间可以很容易地共享数据,但也带来了线程同步和互斥的问题,因为多个线程可能同时访问和修改同一块内存区域。

从linux内核角度来看,进程是承担分配系统资源的基本实体。而线程只是进程内的一个执行分支,是CPU调度的基本单位。

从内核的角度来看,进程是承担分配系统资源的基本实体。内核为进程分配各种资源,如CPU时间片、内存空间、文件描述符等。内核还负责管理进程的生命周期,包括创建、调度、执行、终止等。通过进程,操作系统可以实现多任务处理,使得多个程序能够同时运行在一个计算机上。

2、线程的优缺点

线程相对于进程的优缺点,以及线程在并发编程中的应用场景。下面是详细解释:

优点

  1. 创建一个新线程的代价要比创建一个新进程小得多
    进程是系统分配资源的基本单位,它拥有独立的地址空间、数据栈、文件描述符等资源。因此,创建一个新进程需要分配和初始化这些资源,这通常是一个相对昂贵的操作。而线程是进程的执行单元,它共享进程的资源,因此创建新线程只需要在进程中分配一些必要的资源(如栈空间)即可,这通常比创建新进程要快得多。

  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
    进程切换时,操作系统需要保存当前进程的上下文(如程序计数器、寄存器值、内存管理等),然后加载目标进程的上下文。这个过程涉及到许多寄存器和内存数据的读写,因此开销较大。而线程切换时,由于线程共享进程的地址空间和其他资源,操作系统只需要保存和加载线程的少量上下文(如栈指针和程序计数器),因此开销较小。

    上下文切换的开销:进程切换需要保存和恢复更多的上下文信息,包括进程的程序计数器、寄存器状态、内存映射、I/O状态等。而线程切换只需要保存和恢复线程的上下文信息,由于线程共享同一进程的地址空间,所以线程的上下文信息相对较少。因此,线程切换的开销较小。

    地址空间的切换:进程有独立的地址空间,进程切换时需要切换地址空间的映射关系,这涉及到页表的切换和TLB的刷新等操作,开销较大。而线程共享同一进程的地址空间,线程切换不涉及地址空间的切换,因此开销较小。

    资源开销:由于进程间相互独立,切换两个进程需要保存和恢复更多的资源,包括地址空间、文件描述符等。而线程处于同一个进程内,它们共享进程的资源,因此线程切换的开销通常比进程切换小。

  3. 线程占用的资源要比进程少很多
    由于线程共享进程的地址空间和其他资源,因此每个线程只需要分配一些必要的资源(如栈空间)即可。这使得线程占用的资源比进程要少得多。

  4. 能充分利用多处理器的可并行数量
    多线程编程可以充分利用多处理器系统的并行处理能力。通过将计算任务分解为多个线程,可以让不同的处理器核心同时执行这些线程,从而加速程序的执行。

  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
    在I/O密集型应用中,线程可以在等待慢速I/O操作(如磁盘读写、网络通信等)完成时执行其他计算任务。这种并发执行方式可以显著提高程序的响应速度和吞吐量。

  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
    在计算密集型应用中,将计算任务分解为多个线程并在多处理器系统上并行执行可以显著提高程序的执行效率。通过将计算任务分配给不同的处理器核心,可以充分利用系统的计算能力。

  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
    在I/O密集型应用中,线程可以同时等待多个I/O操作的完成。当一个I/O操作阻塞时,线程可以切换到其他I/O操作或执行其他计算任务,从而避免了资源的浪费。这种重叠I/O操作的方式可以显著提高程序的性能。

缺点

  1. 性能损失

    • 同步和调度开销:当多个线程需要访问共享资源时,必须使用同步机制(如互斥锁、读写锁、条件变量等)来确保数据的一致性和正确性。这些同步机制会带来额外的开销,包括等待锁的释放、线程切换等。当计算密集型线程的数量超过可用的处理器核心数时,这些开销可能变得尤为显著。
    • 线程创建和销毁:虽然线程的创建和销毁开销通常比进程小,但频繁地创建和销毁线程也会带来一定的性能损失。因此,在需要频繁创建和销毁线程的场景中,应该考虑使用线程池等技术来减少这种开销。
  2. 健壮性降低

    • 数据竞争:当多个线程同时访问和修改共享数据时,如果没有正确的同步机制,就可能导致数据竞争和不一致性。这种不一致性可能导致程序出现错误或不可预测的行为。
    • 死锁和活锁:当多个线程相互等待对方释放资源时,就可能发生死锁。死锁会导致线程无法继续执行,从而影响程序的健壮性。活锁则是线程之间不断循环等待对方释放资源,但都没有成功,导致系统资源被无效占用。
  3. 缺乏访问控制:进程是访问控制的基本粒度,而线程则共享同一个进程的地址空间和资源。这意味着在一个线程中调用某些操作系统函数(如文件操作、网络通信等)可能会对整个进程造成影响。因此,在多线程编程中需要特别注意对共享资源的访问控制,以避免潜在的安全风险。

  4. 编程难度高

    • 复杂性增加:多线程编程需要考虑线程间的同步、通信、死锁等问题,这使得程序的逻辑变得更加复杂。
    • 调试困难:多线程程序中的错误往往难以定位和调试,因为线程间的执行顺序和状态可能随时发生变化。

三、Linux线程的实现

1、POSIX线程(Pthreads)

POSIX线程(POSIX Threads,通常简称为Pthreads)是POSIX标准中定义的一组用于多线程编程的API。POSIX是一个开放标准,旨在定义操作系统应该提供的接口,以便软件可以在不同的操作系统之间移植。

在Linux系统中,POSIX线程的实现通常是通过一个名为libpthread的库提供的,这个库包含了实现POSIX线程API所需的功能。#include <pthread.h>是包含Pthreads API声明的头文件。当我们编写使用Pthreads API的多线程程序时,需要包含这个头文件,以便能够使用Pthreads提供的函数和数据类型。

Linux系统自带的libpthread库并不是直接通过系统调用来实现线程的,尽管它可能会使用某些系统调用来完成底层的工作(如创建新线程、设置线程优先级等)。但是,从用户的角度来看,不需要直接与系统调用打交道,因为libpthread库已经封装了这些细节,并提供了更高层次的、更易于使用的接口。

通过Pthreads,程序员可以创建多个线程,每个线程都可以执行程序的不同部分,从而实现并发执行。这些线程共享相同的地址空间(包括代码段、数据段、堆和全局变量),但每个线程都有自己的执行栈和程序计数器。

具体来说,libpthread库将轻量级的系统调用(如果有的话)以及其他的底层机制进行封装,转化为线程相关的接口语义提供给用户。这些接口语义包括线程的创建(pthread_create)、终止(pthread_exit)、等待(pthread_join)、互斥锁(pthread_mutex_t和相关函数)的使用等。通过这些接口,可以方便地在多线程环境中进行编程,而不需要关心底层的具体实现细节。

当我们编写使用多线程的程序时,需要在编译时链接libpthread库。这通常是通过在编译命令中添加-lpthread选项来完成的。例如,如果使用gcc编译器,编译命令可能类似于gcc -o myprogram myprogram.c -lpthread。这样,编译器就会在链接阶段将程序与libpthread库进行链接,以确保程序能够正确地调用Pthreads API。

关于库的使用:Linux动态库与静态库解析

2、线程与进程的联系与区别

同一个进程内的所有线程共享进程的地址空间。这意味着它们都可以访问该地址空间中的任何数据段(例如代码段、数据段、堆和栈)。但是,每个线程有自己的栈(用于局部变量和函数调用),所以它们在自己的栈上的数据是私有的。

在这里插入图片描述

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

📓各线程共享如下资源和环境 :文件描述符表,代码和全局数据,当前用户工作目录,用户id和组id,每种信号的处理方式(SIG_IGNSIG_DFL或者自定义的信号处理函数)。

Linux线程与进程的联系主要体现在以下几个方面:

  1. 共享资源
    • 线程是进程中的一条执行流,因此它们共享其所属进程的大部分资源。这些共享的资源包括地址空间、文件描述符、信号处理器等。
    • 进程是资源分配的基本单位,每个进程都拥有独立的地址空间和其他系统资源。然而,当线程在进程中创建时,它们会共享这些资源。
  2. 调度
    • 进程和线程都可以被系统调度以在不同的时间点上执行。不过,由于线程共享进程的资源,因此线程的切换通常比进程的切换更加高效。
    • 在Linux中,线程的实现是通过轻量级进程来完成的,这使得线程在内核中的调度与进程类似。
  3. 并发执行
    • 进程和线程都可以实现并发执行。多个进程可以同时运行,而在同一个进程内部,多个线程也可以并发执行。
    • 由于线程共享进程的地址空间,因此它们之间的通信和同步通常比进程之间的通信和同步更加高效。

在多线程环境中,每个线程都拥有一些私有的资源,以确保它们能够独立且并发地运行:

  1. 线程的硬件资源(CPU寄存器的值)(调度)
    • CPU寄存器是CPU内部的存储单元,用于存储指令执行过程中产生的数据。在多线程环境中,由于多个线程可能同时运行在CPU上,因此每个线程都需要有自己的寄存器集合来保存其执行过程中的状态和数据。这样,当线程被调度执行时,它可以恢复其之前的状态并从上次中断的位置继续执行。
    • 当从一个线程切换到另一个线程时,操作系统会保存当前线程的寄存器状态,并加载下一个要执行的线程的寄存器状态。这个过程确保了每个线程都能够在其自己的上下文中运行,而不会受到其他线程的影响。
  2. 线程的独立栈结构(常规运行)
    • 栈是一种后进先出(LIFO)的数据结构,用于存储线程执行过程中产生的局部变量、方法调用等信息。每个线程都有自己的独立栈,用于保存其执行历史和状态。
    • 当线程调用一个方法时,会在栈上为该方法分配一个栈帧,用于存储该方法的局部变量和操作数等信息。当方法执行完毕后,其对应的栈帧会被弹出栈,释放占用的内存空间。
    • 线程的独立栈结构确保了每个线程都能够在其自己的内存空间中执行,而不会干扰其他线程的执行。同时,它也为线程之间的数据隔离提供了支持。

📓线程独有的资源:线程ID,寄存器内容,栈,线程局部存储(TLS),信号屏蔽字,调度优先级 ,errno。

下面我们来具体谈一谈线程和进程的区别:

  1. 资源占用:
    • 进程:进程是系统分配资源的基本单位。每个进程都拥有独立的内存空间、系统资源(如文件描述符、信号处理器等)和独立的执行环境(包括程序计数器、堆栈和一组系统寄存器)。
    • 线程:线程是进程的一个执行单元,共享进程所拥有的资源(如内存空间、文件描述符等),但每个线程有自己的栈结构和线程控制块。因此,线程相对于进程来说,资源占用更少,创建和销毁的开销也更小。
  2. 调度和切换:
    • 进程:由于进程拥有独立的内存空间和系统资源,因此进程之间的切换需要保存和恢复更多的上下文信息,这导致了进程切换的开销相对较大。需要切换地址空间和页表。
    • 线程:线程之间的切换只需要保存和恢复线程的上下文信息(如程序计数器、堆栈等),而不需要切换整个进程的上下文,因此线程切换的开销相对较小。不需要切换地址空间和页表。
  3. 通信和同步:
    • 进程:进程之间的通信通常需要通过操作系统提供的进程间通信(IPC)机制来实现,如管道、消息队列、信号量、共享内存等。这些机制的实现相对复杂,且开销较大。
    • 线程:由于线程共享进程的内存空间,因此线程之间的通信和同步相对简单。线程可以通过全局变量等方式进行通信,也可以通过互斥锁、条件变量等同步机制来协调线程的执行。
  4. 独立性:
    • 进程:进程具有独立性,一个进程的崩溃不会影响其他进程的执行。同时,进程之间的隔离性也保证了系统的安全性。
    • 线程:线程属于进程的一部分,一个线程的崩溃可能导致整个进程的崩溃。此外,由于线程共享进程的内存空间,因此线程之间的错误可能会相互影响。
  5. 系统开销:
    • 进程:由于进程拥有独立的资源,因此创建和销毁进程的开销相对较大。同时,进程之间的切换也需要保存和恢复更多的上下文信息,导致系统开销增加。
    • 线程:线程的创建和销毁开销较小,且线程之间的切换开销也较小。这使得线程在需要频繁创建和销毁执行单元的场景中具有优势。

3、轻量级进程(LWP)

在Linux系统中,线程的实现基于轻量级进程(LWP,Lightweight Process)或内核线程的概念。尽管线程与进程共享相同的地址空间,但Linux内核为每个线程都维护了一个独立的task_struct结构体,用于表示线程的状态和相关信息。这使得Linux能够像管理进程一样管理线程,包括调度、优先级设置、同步等。

Linux内核实现线程的方式主要是通过共享进程地址空间的一组线程来完成的。在Linux中,线程也称为轻量级进程(LWP,Lightweight Process)。每个线程都有一个唯一的线程ID(TID)和一个相关的task_struct结构,但所有线程共享同一进程的地址空间(包括代码段、数据段、堆和栈等)。

关于LWP和PID(Process ID,进程ID),这是Linux中用于标识线程和进程的机制:

  1. LWP:LWP是线程在Linux中的一种表示方式,通常用于在工具(如ps命令)中标识线程。每个线程都有一个唯一的LWP ID,这个ID在进程内部是唯一的,但在整个系统中可能不是唯一的(因为不同的进程可以有相同LWP ID的线程)。LWP ID通常用于在调试和性能分析时标识和区分线程。
  2. PID:PID是进程的唯一标识符,它在整个系统中是唯一的。一个进程的所有线程共享同一个PID,因为线程是进程的一部分,它们共享进程的地址空间和资源。因此,即使一个进程内有多个线程,这些线程也会具有相同的PID。

当创建一个线程时,系统会为该线程分配一个唯一的LWP ID,但会将其与父进程的PID关联起来。这样,就可以通过PID和LWP ID的组合来唯一地标识和引用进程中的特定线程。

ps -aL :查看当前系统中的轻量级进程。

while :; do ps -aL | head -1  && ps -aL | grep test ; sleep 1 ; echo "--------" ; done

四、线程控制

1、线程的创建

在POSIX线程(Pthreads)库中,pthread_create() 函数用于创建一个新的线程。这个函数允许在多线程程序中添加并行执行的代码路径。

在这里插入图片描述

参数设置:

  1. pthread_t *thread:这是一个指向 pthread_t 类型的指针,用于存储新创建线程的标识符。pthread_t 是一个不透明的数据类型,用于唯一标识一个线程,是一个输出型参数。
  2. const pthread_attr_t *attr:这是一个指向线程属性对象的指针,用于设置线程的属性,如栈大小、调度策略等。如果不需要设置特定的属性,可以传递 NULL,表示使用默认属性。
  3. void *(*start_routine) (void *):这是新线程开始执行时调用的函数,即线程的入口点。这个函数应该返回一个 void * 类型的指针,通常用于传递线程执行的结果给主线程或其他线程。该函数的参数是一个 void * 类型的指针,用于向线程函数传递参数。
  4. void *arg:这是一个指向任意数据的指针,用于传递给线程函数的参数。这个参数可以是任何类型的数据,但在线程函数中需要将其强制转换为正确的类型。

返回值处理

pthread_create() 函数的返回值是一个整数,用于指示函数调用的成功与否。

  • 0:如果线程创建成功,pthread_create() 返回0。
  • 错误码:如果线程创建失败,pthread_create() 返回一个错误码。你可以使用 perror()strerror() 函数将错误码转换为可读的错误消息。
#include <pthread.h>  
#include <stdio.h>  
#include <stdlib.h>  
  
// 线程函数  
void *my_thread_func(void *arg) {  
    int i;  
    for (i = 0; i < 5; i++) {  
        printf("This is thread function: %d\n", i);  
    }  
    return NULL;  
}  
  
int main() {  
    pthread_t my_thread;  
    int ret;  
  
    // 创建线程  
    ret = pthread_create(&my_thread, NULL, my_thread_func, NULL);  
    if (ret != 0) {  
        perror("Failed to create thread");  
        exit(EXIT_FAILURE);  
    }  
  
    // 等待线程结束  
    pthread_join(my_thread, NULL);  
  
    printf("Main thread exiting\n");  
    return 0;  
}

在这个示例中,我们创建了一个简单的线程,它打印出5个消息。如果线程创建失败,程序会打印出错误消息并退出。如果线程创建成功,主线程会等待该线程执行完毕后再继续执行,并打印出“Main thread exiting”

线程ID(通常缩写为tid)是一个唯一标识符,用于区分进程中的不同线程。当你创建一个新的线程时,pthread_create函数会返回一个线程ID,这个ID可以用来引用和操作该线程。如:pthread_t tid; 定义了一个变量 tid,用于存储新创建的线程的ID。pthread_create(&tid, nullptr, newthreadrun, nullptr); 调用会创建一个新线程,并将新线程的ID存储在 tid 中。线程ID是系统用来跟踪和管理线程的内部标识符。是区分不同线程的唯一标识符。

这些底层的轻量级进程并不是由Linux内核直接暴露给用户的。相反,它们是通过库(如POSIX线程库,也称为pthreads)来管理的,这些库为用户提供了创建、管理和同步线程的高级接口。

  1. 轻量级进程(LWP):在Linux内核中,线程是通过轻量级进程来实现的。这些LWP与常规进程(由fork创建)在内核中的表示非常相似,但LWP与创建它的进程共享相同的地址空间和某些其他资源。
  2. 线程库(如Pthreads):当我们在用户空间使用线程库(如POSIX线程库,简称Pthreads)创建线程时,这些库会为我们处理底层的细节。具体来说,Pthreads库会调用clone系统调用来请求内核创建一个新的LWP。但是,库还负责处理许多其他事情,如线程的同步、调度和取消等。

总之,虽然Linux中的线程在底层是通过轻量级进程来实现的,但线程库(如Pthreads)为我们提供了更高级的抽象和更多的功能。这些库负责处理底层的细节,使我们能够更方便地使用线程进行并发编程。

在Linux中线程创建在共享区。

在Linu中,线程与进程在许多方面都是相似的,但也有一些关键的差异。当我们在Linux上讨论线程时,理解它们是如何与进程内存空间交互的非常重要。

首先,要明确的是,线程是进程的执行单元。在Linux中,线程与进程共享以下资源:地址空间,文件描述符,信号处理器等。

然而,线程也有自己的资源,例如:线程ID,栈,寄存器状态(每个线程都有自己的CPU寄存器状态,包括程序计数器、栈指针等。)信号屏蔽字。

现在,回到为什么线程创建在“共享区”的问题:

在这里插入图片描述

  • 当一个进程创建新的线程时,新线程与原始线程(或其他已存在的线程)共享相同的地址空间。这是因为线程设计的初衷就是为了在共享内存空间中并发执行代码,从而更容易地共享数据和资源。
  • 通过共享地址空间,线程可以更快地访问和修改数据,因为它们不需要像进程那样通过内核进行上下文切换和数据复制。
  • 当然,由于线程共享内存,因此必须小心处理数据竞争和同步问题。否则,可能会导致未定义的行为或错误的结果。

总结:Linux中的线程被创建在进程的共享地址空间中,以利用并发执行的优点,同时共享数据和资源。然而,这也带来了数据竞争和同步的问题,需要开发者特别注意。

Linux操作系统是如何找到我们通过库函数调用在共享区创建线程的呢?

clone系统调用是pthread_create的底层实现。当在Linux系统中使用库函数创建线程时,实际上底层可能会使用clone系统调用来实现。clone系统调用允许创建一个新的进程,但与传统的fork系统调用不同,clone提供了更细粒度的控制,允许子进程与父进程共享资源,如内存空间、文件描述符和信号处理器等。

在Linux中,虽然从用户空间的角度来看,线程是由库函数创建的,但实际上这些库函数在底层会利用clone系统调用来实现线程的创建。clone系统调用允许新创建的线程与父线程(即创建它的线程)共享某些资源,如内存空间、文件描述符和信号处理器等。

当库函数(如pthread_create)被调用时,它会设置必要的参数,包括要共享的资源、线程的栈大小、优先级等,然后调用clone系统调用来实际创建线程。操作系统内核会处理这个调用,并根据提供的参数创建新线程,并为其分配必要的资源。

因此,无论是通过库函数调用还是直接调用系统调用来创建线程,Linux系统都会利用clone系统调用的功能来实现线程的创建和资源共享。这使得多线程编程在Linux系统中变得更加灵活和高效。

在通过库函数创建线程时,操作系统会执行以下步骤来找到和管理这些线程:

  1. 库函数调用:首先,程序会调用库函数(如pthread_create)来请求创建一个新线程。
  2. 封装clone调用:库函数内部会封装对clone系统调用的调用。clone系统调用允许程序指定要共享哪些资源,以及新线程的开始执行点。
  3. 设置线程属性:在调用clone之前,库函数会根据提供的线程属性(如栈大小、优先级等)来设置相关参数。
  4. 执行clone调用:库函数会执行clone系统调用,传递必要的参数。操作系统内核会处理这个调用,并创建一个新的线程。
  5. 分配资源:操作系统内核为新线程分配必要的资源,如内存空间、栈等。这些资源可能是从现有的共享资源中分配出来的,也可能是为新线程单独分配的。
  6. 将新线程加入调度队列:一旦新线程的资源被分配并设置好,操作系统会将其加入到调度队列中,等待调度器选择执行。
  7. 线程调度和执行:调度器会根据一定的算法从调度队列中选择一个线程来执行。当调度器选择到新创建的线程时,它会开始执行线程的代码。

通过clone系统调用,操作系统可以精确地控制新线程的创建过程,并允许线程之间共享资源。这使得多线程编程更加灵活和高效。在Linux系统中,clone系统调用是实现多线程编程的重要基础。

我们先看如下代码,线程在进程地址空间中的虚拟地址就称之为tid。

#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
std::string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%lx", tid);
    return id;
}
void *thread_func(void *arg)
{
    std::string name = static_cast<char *>(arg);

    int cnt = 5;
    while (cnt)
    {
        sleep(1);
        printf("Thread is running... %d\n", cnt--);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, (void *)"thread-01");
    std::cout << "main thread id : " << pthread_self() << " ," << ToHex(pthread_self()) << std::endl;
    std::cout << "new thread id : " << tid << " ," << ToHex(tid) << std::endl;

    int n = pthread_join(tid, nullptr);

    printf("Main thread wait return , errno : %d, stat: %s\n", n, strerror(n));
    return 0;
}

输出结果:

zyb@myserver:~/study_code/thread_study/demo10$ ./test_thread
main thread id : 140454761211712 ,0x7fbe2c262740
new thread id : 140454761207360 ,0x7fbe2c261640
Thread is running… 5
Thread is running… 4
Thread is running… 3
Thread is running… 2
Thread is running… 1
Main thread wait return , errno : 0, stat: Success

zyb@myserver:~/study_code/demo$ ps -aL | head -1 && ps -aL | grep test_thread
PID     LWP TTY          TIME CMD
34159   34159 pts/3    00:00:00 test_thread
34159   34160 pts/3    00:00:00 test_thread

我们可以发现tid与LWP值不同。

Linux系统支持线程,并且这些线程在内核级别被实现为轻量级进程。然而,从用户空间的角度看,这些线程是通过POSIX线程(pthread)库来管理和使用的。 用户(或应用程序开发者)可以通过pthread库提供的接口来管理线程。

线程控制块(TCB, Thread Control Block)是内核用来管理线程的数据结构,它包含了线程的各种信息(如状态、优先级、栈信息等)。而tid(线程ID)是一个用户空间标识符,用于pthread库标识和引用线程。线程TCB的起始地址就是线程的tid。

每个线程通常都有自己独立的栈结构,这个栈结构是由操作系统在创建线程时分配的,并且由pthread库和内核共同维护。这个栈用于存储线程的局部变量、函数调用信息等。在Linux上,线程的栈通常是通过mmap系统调用来分配的,并且可以在创建线程时通过pthread_attr_t属性对象来设置栈的大小和其他属性。

在Linux中,mmap(Memory Map)是一个系统调用,它允许程序将一个文件或设备的一部分或其他对象映射进内存。但是,在创建线程上下文中,mmap通常被用于动态地分配内存区域,特别是为线程栈分配内存。

当Linux内核创建一个新线程时,它并不总是从进程的堆或数据段中分配栈空间。相反,它可能会使用mmap系统调用来请求一个私有的、匿名的内存区域,该区域将用作新线程的栈。这种方法的优点是它允许内核更直接地管理栈内存,并可能提供更好的性能和隔离性。

总的来说,mmap是一个强大的系统调用,它允许程序以灵活的方式管理内存。在创建线程时,它可能被用作一种机制来分配和管理线程栈。

下面我们来证明线程有独立栈结构:

无论是单线程程序还是多线程程序,每次函数被调用时,都会在其调用栈上创建一个新的栈帧(Stack Frame)。这个栈帧包含了函数调用的所有信息,比如函数的返回地址、传递给函数的参数以及函数内部的局部变量。

#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>

std::string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%lx", tid);
    return id;
}
void *thread_func(void *arg)
{
    std::string name = static_cast<char *>(arg);

    int cnt = 5;
    while (cnt--)
    {
        sleep(1);
        std::cout << name << " :" << getpid() << " ,cnt: " << cnt << " , &cnt : " << &cnt << std::endl;
    }
    return nullptr;
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_create(&tid1, NULL, thread_func, (void *)"thread-01");
    pthread_create(&tid2, NULL, thread_func, (void *)"thread-02");

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

    return 0;
}

运行结果:

zyb@myserver:~/study_code/demo$ ./test_thread
thread-02 :80192 ,cnt: 4 , &cnt : 0x7f9b5168ae0c
thread-01 :80192 ,cnt: 4 , &cnt : 0x7f9b51e8be0c
thread-02 :80192 ,cnt: 3 , &cnt : 0x7f9b5168ae0c
thread-01 :80192 ,cnt: 3 , &cnt : 0x7f9b51e8be0c
thread-02 :80192 ,cnt: 2 , &cnt : 0x7f9b5168ae0c
thread-01 :80192 ,cnt: 2 , &cnt : 0x7f9b51e8be0c
thread-02 :80192 ,cnt: 1 , &cnt : 0x7f9b5168ae0c
thread-01 :80192 ,cnt: 1 , &cnt : 0x7f9b51e8be0c
thread-02 :80192 ,cnt: 0 , &cnt : 0x7f9b5168ae0c
thread-01 :80192 ,cnt: 0 , &cnt : 0x7f9b51e8be0c

我们发现打印出来的cnt的地址是不一样的。

在多线程环境中,每个线程都有自己的调用栈。因此,当两个线程同时进入同一个函数时,每个线程都会在它自己的调用栈上创建一个新的栈帧。这两个栈帧是独立的,分别属于不同的线程,并且存储着各自线程调用该函数时的参数和局部变量。

这样的设计使得每个线程都能够独立地执行代码,而不会受到其他线程的影响(除了可能的共享内存访问冲突等问题)。每个线程都可以在自己的栈帧上操作自己的局部变量,而不会影响到其他线程的局部变量。

需要注意的是,虽然每个线程都有自己的调用栈和栈帧,但是它们可能会共享一些数据,比如全局变量、静态变量以及通过某种方式(如指针或引用)传递的共享内存。在编写多线程程序时,需要特别注意这些共享数据的访问和修改,以避免出现数据竞争(Data Race)和其他并发问题。

下面我们来证明线程可以访问全局变量且共享:

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

int g_val = 100; // 全局变量被共享

void *thread_func1(void *arg)
{
    std::string name = static_cast<char *>(arg);

    int cnt = 5;
    while (cnt--)
    {
        sleep(1);
        std::cout << name << " :" << " ,g_val: " << g_val << " , &g_val : " << &g_val << std::endl;
    }
    return nullptr;
}
void *thread_func2(void *arg)
{
    std::string name = static_cast<char *>(arg);

    int cnt = 5;
    while (cnt--)
    {
        sleep(1);
        std::cout << name << " :" << " ,g_val: " << g_val << " , &g_val : " << &g_val << std::endl;
        g_val--;
    }
    return nullptr;
}
int main()
{
    printf("main thread, g_val: %d, &g_val: %p\n", g_val, &g_val);
    pthread_t tid1;
    pthread_t tid2;
    pthread_create(&tid1, NULL, thread_func1, (void *)"thread-01");
    pthread_create(&tid2, NULL, thread_func2, (void *)"thread-02");

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

    return 0;
}

zyb@myserver:~/study_code/demo$ ./test_thread
main thread, g_val: 100, &g_val: 0x564fcd300010
thread-01 : ,g_val: 100 , &g_val : 0x564fcd300010
thread-02 : ,g_val: 100 , &g_val : 0x564fcd300010
thread-01 : ,g_val: 99 , &g_val : 0x564fcd300010
thread-02 : ,g_val: 99 , &g_val : 0x564fcd300010
thread-01 : ,g_val: 98 , &g_val : 0x564fcd300010
thread-02 : ,g_val: 98 , &g_val : 0x564fcd300010
thread-01 : ,g_val: 97 , &g_val : 0x564fcd300010
thread-02 : ,g_val: 97 , &g_val : 0x564fcd300010
thread-01 : ,g_val: 96 , &g_val : 0x564fcd300010
thread-02 : ,g_val: 96 , &g_val : 0x564fcd300010

g_val 是一个全局变量,它在 main 函数和两个线程函数 thread_func1thread_func2 中都可以被访问。

thread_func1 只是读取 g_val 的值并打印出来,而 thread_func2 在读取 g_val 的值后还会将其减一。

我们可以发现,这两个线程都在访问 g_val,它们都在共享这个全局变量。此外 g_val 的地址都相同,因此所有线程都在访问内存中的同一个位置。

2、线程的等待

与进程相似,线程也需要wait,否则会产生类似进程那里的内存泄露问题。使用pthread_join()等待线程结束并获取其返回值。

pthread_join()函数用于等待一个特定的线程终止。当一个线程完成时,它的状态会变为"terminated"(已终止),但它的资源(如栈内存)不会被立即释放,除非有其他线程调用pthread_join()来回收这些资源。

pthread_join()函数会阻塞调用线程,直到指定的线程终止。一旦目标线程终止,pthread_join()将回收其资源,并通过一个指向void的指针返回目标线程的返回值(如果有的话)。

  • thread 是你想要等待的线程的标识符。
  • retval 是一个指向void*的指针,用于存储线程的返回值。如果不关心线程的返回值,可以将这个参数设置为NULL

下面是使用pthread_join()的一个基本示例:

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

const int threadnum = 5;
class Task
{
public:
    Task(int x, int y) : datax(x), datay(y) {}
    int Execute() { return datax + datay; }
    ~Task() {}

private:
    int datax;
    int datay;
};
class ThreadData : public Task
{
public:
    ThreadData(int x, int y, const std::string &threadname)
        : Task(x, y), _threadname(threadname) {}
    std::string threadname() { return _threadname; }
    int run() { return Execute(); }

private:
    std::string _threadname;
};
class Result
{
public:
    Result(int result, const std::string &threadname) : _result(result), _threadname(threadname) {}
    ~Result() {}

    void Print()
    {
        std::cout << _threadname << " : " << _result << std::endl;
    }

private:
    int _result;
    std::string _threadname;
};

void *handerTask(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    Result *res = new Result(td->run(), td->threadname());
    delete td;

    sleep(2);
    return res;
}
 
int main()
{
    std::vector<pthread_t> threads;
    for (int i = 0; i < threadnum; i++)
    {
        char threadname[64];
        snprintf(threadname, 64, "Thread-%d", i + 1);
        ThreadData *td = new ThreadData(20 + i + 1, 30 + i + 1, threadname);

        pthread_t tid;
        pthread_create(&tid, nullptr, handerTask, td);

        threads.push_back(tid);
    }

    std::vector<Result *> result_set;
    void *ret = nullptr;

    for (auto &tid : threads)
    {
        pthread_join(tid, &ret);
        result_set.push_back((Result *)ret);
    }

    for (auto &res : result_set)
    {
        res->Print();
        delete res;
    }
}

类的设计

  • Task 类:这是一个简单的类,用于执行加法操作(Execute())。
  • ThreadData 类:继承自 Task 类,并添加了一个线程名称 _threadname。这个类还提供了一个 run() 方法,它实际上只是调用了 Execute()
  • Result 类:用于存储线程的执行结果和线程名称。

线程创建和函数handerTask 函数:这是线程执行的函数。它接收一个 void* 类型的参数(实际上是 ThreadData* 的一个实例),执行加法操作,并创建一个 Result 对象。然后它释放了 ThreadData 对象,并休眠了2秒。最后,它返回了 Result*

代码中创建了一个 vector<pthread_t> 来存储线程ID,循环创建多个线程,每个线程都执行 handerTask 函数,并传递一个 ThreadData 对象作为参数。使用 pthread_join 等待每个线程完成,并将返回的结果(Result*)存储在 vector<Result*> 中。使用了 pthread_join 来等待线程完成,并获取了线程返回的结果(Result*)。

3、线程的终止

  1. 正常退出

当线程完成了其任务并正常退出时,它可以通过以下几种方式来实现:

  • 函数返回:线程执行的函数(通常是pthread_create指定的函数)执行完毕并返回时,线程将正常退出。

  • 调用pthread_exit():线程可以在任何时候调用pthread_exit()函数来立即退出。该函数接受一个指向void的指针作为参数,该指针可以被其他线程通过pthread_join()函数获取。

  • 主线程返回:如果主线程(即调用pthread_create()创建其他线程的线程)执行完毕并返回,那么整个进程(包括所有线程)将终止。但是,其他线程在此之前应该已经正常退出或被终止。

  1. 异常退出

线程也可能由于异常或错误而退出,这些异常或错误通常是由于编程错误或不可预测的运行时错误引起的。

  • 未捕获的异常:当线程遇到无法恢复的异常(如除以零、野指针访问等)时,操作系统通常会向进程发送一个信号(如SIGSEGVSIGFPE等)。如果进程没有安装信号处理器来捕获这些信号,或者信号处理器没有适当地处理它们,那么整个进程可能会被终止。

  • 调用pthread_cancel():虽然这不是真正的“异常”退出,但pthread_cancel()函数允许一个线程请求另一个线程终止其执行。被请求的线程可以选择立即终止,或者在达到某个取消点(cancellation point)时终止。取消点通常是某些库函数调用时的点,在这些点上,线程会检查是否有取消请求。

处理线程异常退出的策略

  • 设置信号处理器:对于可能导致进程终止的信号,如SIGSEGVSIGFPE等,可以设置信号处理器来捕获这些信号并尝试恢复或优雅地终止进程。然而,由于线程共享相同的地址空间,处理这些信号可能会很复杂。
  • 使用线程取消状态处理程序:如果使用了pthread_cancel()来请求线程终止,可以设置一个取消状态处理程序(cancellation handler)来处理取消请求。这个处理程序可以在线程响应取消请求之前执行一些清理工作。
  • 日志和监控:在程序中实现日志记录和监控机制,以便在出现异常时能够及时发现并解决问题。

需要注意的是,虽然线程是进程的执行单元,并且它们共享同一个地址空间,但线程的异常退出并不一定总是导致整个进程的终止。这取决于操作系统、信号处理器的设置以及异常的性质和处理方式。

然而,在多线程环境中,一个线程的异常退出通常会对整个进程的状态和行为产生重大影响,因此需要谨慎处理。

这还是因为所有线程共享同一个进程地址空间,且操作系统通常将进程视为一个整体来处理,所以当进程收到一个致命信号时,它会终止整个进程,包括进程内的所有线程。这是因为操作系统通常无法安全地只终止一个线程而不影响其他线程的状态和数据。

简单来说,线程退出分为三种情况:

  1. 代码跑完,结果对:如果线程正常执行完毕,并且没有遇到任何问题,那么它就可以正常退出。线程的退出并不会导致整个进程的终止,除非这是进程中的最后一个线程。
  2. 代码跑完,结果不对:如果线程的代码执行完毕但结果不正确,这通常是由于逻辑错误、数据竞争、未同步的访问或其他并发问题导致的。这种情况不会直接导致进程终止,但可能会导致程序行为异常或数据损坏。
  3. 出异常了:当一个线程遇到无法恢复的异常时(如除以零、野指针访问等),操作系统通常会向进程发送一个信号(如SIGSEGVSIGFPE)。默认情况下,这些信号会导致进程终止。因为线程共享进程的地址空间,所以一个线程中的异常通常会导致整个进程的终止。

关于exit函数,它是用来终止整个进程的,而不是单个线程。在多线程环境中,调用exit会导致整个进程的终止,包括所有正在运行的线程。

因此,通常不建议在线程中使用exit来退出线程。相反,应该使用线程特定的退出机制,如POSIX线程(pthreads)中的pthread_exit函数或pthread_cancel函数。

在这里插入图片描述

  • retval 是一个指向要返回给调用 pthread_join 的线程的值的指针。如果线程被取消(通过 pthread_cancel),或者主线程(在创建它的进程中)返回或调用 exit,则这个值可能不会被接收。

当一个线程调用 pthread_exit 时,它会立即停止执行,并释放由线程占用的资源(如线程栈)。但是,线程的终止状态并不会立即通知给其他线程,除非其他线程调用了某种形式的等待函数(如 pthread_join)来等待这个线程的结束。

#include <pthread.h>  
#include <iostream>  
  
void *thread_func(void *arg) {  
    printf("Thread is running...\n");  
    pthread_exit((void *)1);  // 线程将退出,并返回一个指向整数值1的指针  
}  
  
int main() {  
    pthread_t thread;  
    void *retval;  
  
    pthread_create(&thread, NULL, thread_func, NULL);  
  
    pthread_join(thread, &retval);  // 等待线程结束,并获取其返回值  
    printf("Thread returned: %ld\n", (long)retval);  // 打印线程的返回值  
  
    return 0;  
}

在这个示例中,线程函数 thread_func 打印一条消息,然后调用 pthread_exit 来退出线程。主线程使用 pthread_join 等待线程结束,并获取其返回值。

如果主线程中保证了新线程已经启动,我们就可以用pthread_cancel函数取消新线程。该函数用于向指定的线程发送取消请求,以请求该线程终止执行。

当一个线程被pthread_cancel函数取消时,它的返回结果会被设置为PTHREAD_CANCELED。在POSIX线程(pthreads)API中,PTHREAD_CANCELED是一个宏,通常定义为-1,但它实际上是一个特殊的值,用于表示线程是由于取消操作而终止的。

#define PTHREAD_CANCELED ((void *) -1)

当线程函数返回时,它实际上返回的是一个指向void*的指针,但在许多情况下,这个指针被用作一个错误代码或状态码。然而,对于取消的线程,这个指针不会被设置,而是线程的退出状态会被设置为PTHREAD_CANCELED

在这里插入图片描述

  • thread:这是目标线程的线程标识符,类型为 pthread_t

下面是一个简单的示例,展示了如何使用pthread_cancelpthread_join来取消一个线程并检查其退出状态:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void *thread_func(void *arg)
{
    int cnt = 5;
    while (cnt--)
    {
        sleep(1);
        printf("Thread is running... %d\n", cnt);
    }
    return NULL; // 这个return实际上永远不会被执行,因为线程被取消了
}

int main()
{
    pthread_t thread;
    void *result;
    int rc;

    // 创建线程
    rc = pthread_create(&thread, NULL, thread_func, NULL);
    if (rc)
    {
        printf("Error: return code from pthread_create() is %d\n", rc);
        exit(-1);
    }

    // 等待一段时间,然后尝试取消线程
    sleep(5);
    printf("Canceling thread...\n");
    pthread_cancel(thread);

    // 等待线程退出并获取其退出状态
    rc = pthread_join(thread, &result);
    if (rc)
    {
        printf("Error: return code from pthread_join() is %d\n", rc);
        exit(-1);
    }

    // 检查线程的退出状态
    if (result == PTHREAD_CANCELED)
    {
        printf("Thread was canceled\n");
    }
    else
    {
        printf("Thread exited with status %p\n", result);
    }

    printf("Main thread exiting\n");
    return 0;
}

在这个示例中,当线程被取消时,pthread_join会成功返回,并且result指针将指向PTHREAD_CANCELED。然后我们可以检查这个值来确定线程是否被取消。

⚠️线程不能直接调用 pthread_cancel 来取消自己。线程可以调用 pthread_exit 来立即终止自己,并返回一个指向退出状态的指针。其他线程可以通过 pthread_join 来获取这个退出状态。

下面我们看如下代码,若主线程先退出会怎么样?

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
std::string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%lx", tid);
    return id;
}
void *newthreadrun(void *args)
{
    std::string name = (char *)args;
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "I am  " << name << " ,"
                  << " pid: " << getpid()
                  << " , my thread id: "
                  << ToHex(pthread_self())
                  << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, newthreadrun, (void *)"thread-1");
    sleep(1);
    std::cout << " main thread quit " << std::endl;

    return 0;
}

输出结果:

zyb@myserver:~/study_code/demo$ ./test_thread
I am thread-1 , pid: 80151 , my thread id: 0x7fed243c8640
main thread quit

我们可以发现若主线程退出,那么整个进程内的线程都退出。

在大多数操作系统和线程模型中,如果主线程(通常也称为“主线程”或“初始线程”)退出,那么整个进程就会终止,无论是否还有其他线程正在运行。这是因为主线程是进程的入口点,当主线程退出时,操作系统会清理进程占用的所有资源,包括其他线程所占用的资源。

当主线程执行完其任务并退出时,它会释放其占用的所有资源,并通知操作系统进程已经完成。操作系统随后会清理进程占用的所有剩余资源,并终止进程的执行。

如果进程中有其他线程仍在运行,并且主线程没有等待它们完成(即没有调用pthread_join或其他相应的等待机制),那么这些线程将会被强制终止,而不会有机会完成它们的任务或执行清理操作。这可能导致数据丢失或其他不可预知的行为。因此需要保证主线程最后退出。

4、线程分离

线程分离(detaching a thread)是线程管理中的一个概念,它指的是线程在创建后不需要被其他线程(通常是创建它的线程)显式地等待其结束(通过调用pthread_join函数)。当线程被设置为分离状态时,系统会在线程结束时自动释放线程所占用的资源,而不需要其他线程来回收这些资源。

在这里插入图片描述

如何理解线程分离: 分离只是线程的工作状态,底层依旧属于同一个进程。分离仅仅是不需要等待了。

  1. 资源回收:在POSIX线程(pthreads)中,当一个线程结束时,它所占用的栈空间和其他资源并不会立即被释放,除非有另一个线程调用pthread_join来回收这些资源。如果线程被设置为分离状态,那么当线程结束时,这些资源会自动被系统回收,而不需要其他线程介入。

  2. 主线程不关心:当主线程(或其他线程)创建一个新线程并设置其为分离状态时,主线程就不再需要关心这个新线程的执行结果和结束时间。也就是说,主线程不需要调用pthread_join来等待新线程结束。

  3. join函数的行为:如果一个线程被设置为分离状态,并且你尝试对它调用pthread_join,那么pthread_join会返回错误(通常是EINVAL)。这是因为分离状态的线程不允许被其他线程join。

  4. 底层仍属于同一进程:尽管线程被设置为分离状态,但它仍然是创建它的进程的一部分。这意味着线程可以访问和修改该进程的共享内存区域,并且可以访问该进程打开的文件描述符等。

  5. 不需要等待:这是线程分离最直观的特点。一旦线程被设置为分离状态,你就不需要(也不能)等待它结束。这可以提高程序的并发性和响应性,但也可能增加程序管理的复杂性,特别是当多个线程需要协调其活动时。

如果尝试对一个已经分离的线程调用pthread_joinpthread_join函数将返回错误EINVAL(表示无效的参数),但并不会直接导致进程退出。它只是告诉调用者该线程已经被分离,不能通过pthread_join来等待。这是调用pthread_join时可能遇到的错误处理的一个例子:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>

void *thread_function(void *arg)
{
    // 执行一些任务...
    std::cout << "Thread function is running...\n";
    return nullptr;
}

int main()
{
    pthread_t thread_id;
    int result = pthread_create(&thread_id, nullptr, thread_function, nullptr);
    if (result != 0)
    {
        std::cerr << "Error: pthread_create failed\n";
        return 1;
    }

    // 将线程设置为分离模式
    result = pthread_detach(thread_id);
    if (result != 0)
    {
        std::cerr << "Error: pthread_detach failed\n";
        // 注意:即使 pthread_detach 失败,线程仍然会运行,但你需要处理错误
    }

    // 尝试连接一个已经分离的线程(仅用于演示错误处理)
    void *thread_return;
    result = pthread_join(thread_id, &thread_return);
    if (result == EINVAL)
    {
        std::cerr << "Error: pthread_join on a detached thread\n";
        // 这里只是报告错误,不会退出进程
    }
    else if (result != 0)
    {
        std::cerr << "Error: pthread_join failed with unexpected error\n";
        return 1;
    }
    int cnt = 3;
    while (cnt--)
    {
        std::cout << "cnt: " << cnt << std::endl;
        sleep(1);
    }
    // 主线程继续执行其他任务,或者退出
    std::cout << "Main thread continuing...\n";
    return 0;
}

zyb@myserver:~/study_code/demo$ ./test_thread
Error: pthread_join on a detached thread
cnt: 2
Thread function is running…
cnt: 1
cnt: 0
Main thread continuing…

在这个例子中,如果尝试对一个已经分离的线程调用pthread_join,程序会输出一个错误消息,但会继续执行并正常退出。不会直接导致进程退出,除非在错误处理代码中显式地调用了exit或其他终止进程的函数。

5、线程的局部存储

__thread(有时也写作thread_local,这是C++11标准中的关键字)是一个存储类修饰符,它告诉编译器这个变量是线程局部的(thread-local)。这意味着每个线程都会拥有这个变量的一个副本,放在本线程的局部存储,对这个变量的修改不会影响其他线程中该变量的值。

这意味着每个线程都会拥有该变量的一个独立副本,不同线程之间的这个变量副本互不干扰。这种变量对于需要在线程之间保持独立状态的情况特别有用。

只能用于存储内置类型,C++中的vectorstring等不能存储。

#include <pthread.h>
#include <iostream>
#include <unistd.h>
 
__thread int g_val = 100; // 全局变量被共享
std::string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%lx", tid);
    return id;
}
void *thread_func1(void *arg)
{
    std::string name = static_cast<char *>(arg);

    int cnt = 5;
    while (cnt--)
    {
        sleep(1);
        std::cout << name << " :" << getpid() << " ,g_val: " << g_val << " , &g_val : " << &g_val << std::endl;
    }
    return nullptr;
}
void *thread_func2(void *arg)
{
    std::string name = static_cast<char *>(arg);

    int cnt = 5;
    while (cnt--)
    {
        sleep(1);
        std::cout << name << " :" << getpid() << " ,g_val: " << g_val << " , &g_val : " << &g_val << std::endl;
        g_val--;
    }
    return nullptr;
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_create(&tid1, NULL, thread_func1, (void *)"thread-01");
    pthread_create(&tid2, NULL, thread_func2, (void *)"thread-02");
    std::cout << "main thread id : " << pthread_self() << " ," << ToHex(pthread_self()) << std::endl;
    std::cout << "new thread1 id : " << tid1 << " ," << ToHex(tid1) << std::endl;
    std::cout << "new thread2 id : " << tid2 << " ," << ToHex(tid2) << std::endl;

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

    return 0;
}

观察上面代码的运行结果。在这个示例中,g_val 是一个线程局部变量。当我们在 thread_func* 中访问它时,我们实际上是在访问当前线程的副本。

请注意,虽然 __thread 在GCC和其他一些编译器中得到了支持,但它并不是C++标准的一部分。从C++11开始,标准库提供了 thread_local 关键字作为线程局部存储的官方支持。因此,如果正在编写可移植的代码,建议使用 thread_local 而不是 __thread

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

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

相关文章

计算机操作系统体系结构

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天给大家讲讲操作系统。 当今的操作系统趋向于越来越复杂&#xff0c;因为它们提供许多服务&#xff0c;并支持各种硬件和软件资源&#xff08;请参见“操作系统思想&#xff1a;尽量保持简单”&#xff0…

Dynadot API调整一览

关于Dynadot Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮…

DISCO: Disentangled Control for Realistic Human Dance Generation

NTU&Microsoft CVPR24https://github.com/Wangt-CN/DisCo 问题引入 提高human motion transfer模型的泛化性&#xff1b;给出 f , g f,g f,g作为参考图片的前背景&#xff0c;然后给出单个pose p p t pp_t ppt​或者pose序列 p { p 1 , p 2 , ⋯ , p T } p \{p_1,p_2…

撤销最近一次的提交,使用git revert 和 git reset的区别

文章目录 工作区 暂存区 本地仓库 远程仓库需求&#xff1a;已推送到远程仓库&#xff0c;想要撤销操作git revert &#xff08;添加新的提交来“反做”之前的更改&#xff0c;云端会残留上次的提交记录&#xff09;git reset&#xff08;相当于覆盖上次的提交&#xff09;1.--…

HIGT:用于全景切片图像分析的层次交互图-Transformer

文章目录 HIGT: Hierarchical Interaction Graph-Transformer for Whole Slide Image Analysis摘要方法实验结果 HIGT: Hierarchical Interaction Graph-Transformer for Whole Slide Image Analysis 摘要 在计算病理学领域&#xff0c;全景切片图像&#xff08;WSIs&#xf…

JavaEE-Spring Controller(服务器控制以及Controller的实现和配置)

Spring Controller 服务器控制 响应架构 Spring Boot 内集成了 Tomcat 服务器&#xff0c;也可以外接 Tomcat 服务器。通过控制层接收浏览器的 URL 请求进行操作并返回数据。 底层和浏览器的信息交互仍旧由 servlet 完成&#xff0c;服务器整体架构如下&#xff1a; Server&…

调整表格大小

方法一&#xff1a;使用鼠标拖动表格边框或右下角的调整控点 在Word文档中&#xff0c;选中要缩小的表格&#xff0c;将鼠标指针放在表格的边框线上&#xff0c;直到指针变成双箭头的形状。 按住鼠标左键&#xff0c;拖动边框线&#xff0c;调整表格的宽度或高度。如果同时按住…

01 一文理解,Prometheus详细介绍

01 一文理解&#xff0c;Prometheus详细介绍 介绍 大家好&#xff0c;我是秋意零。 Prometheus 是一个开源的系统监控和报警工具包&#xff0c;最初由SoundCloud开发&#xff0c;并在2012年作为开源项目发布。Prometheus 目前由Cloud Native Computing Foundation&#xff08…

python爬虫之pandas库——数据清洗

安装pandas库 pip install pandas pandas库操作文件 已知在本地桌面有一名为Python开发岗位的csv文件(如果是excel文件可以做简单修改即可&#xff0c;道理是通用的) 打开文件&#xff1a; 打开文件并查看文件内容 from pandas import DataFrame import pandas as pd data_c…

AIGC 010-CLIP第一个文本和图像对齐的大模型!

AIGC 010-CLIP第一个文本和图像对齐的大模型&#xff01; 文章目录 0 论文工作1 论文方法2 效果 0 论文工作 不客气的说CLIP和扩散模型的成功让计算式视觉领域几乎所有工作都重新做了一遍。 CLIP&#xff08;对比语言-图像预训练&#xff09;论文提出了一种新的对比学习方法&a…

【C++课程学习】:二叉树的基本函数实现

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;C课程学习 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 &#x1f349;二叉树的结构类型&#xff1a; &#x1f349;1.创建二叉树函数&#xff08;根据数组&am…

如何将云服务器上操作系统由centos切换为ubuntu

本文将介绍如何将我们购买的云服务器上之前装的centos切换为ubuntu&#xff0c;云服务器以华为云为例&#xff0c;要切换的ubuntu版本为ubuntu20.04。 参考官方文档&#xff1a;切换操作系统_弹性云服务器 ECS (huaweicloud.com) 首先打开华为云官网&#xff0c;登录后点击右…

机器学习(五) -- 监督学习(5) -- 线性回归1

系列文章目录及链接 上篇&#xff1a;机器学习&#xff08;五&#xff09; -- 监督学习&#xff08;4&#xff09; -- 集成学习方法 - 随机森林 下篇&#xff1a;机器学习&#xff08;五&#xff09; -- 监督学习&#xff08;5&#xff09; -- 线性回归2 前言 tips&#xff1…

树莓派指令

1.常用指令 2.在终端窗口编辑文本文件 2.1nano编辑器 在文本里ctrlG就可以查看更多的快捷按键 2.2vi编辑器 进入默认为命令模式

Spring-Cloud-OpenFeign源码解析-04-调用流程分析

在Spring-Cloud-OpenFeign源码解析-03-FeignClientFactoryBean分析到&#xff0c;通过Autowired或者Resource注入FeignClient实例的时候&#xff0c;实际上返回的是JDK动态代理对象&#xff0c;具体的实现逻辑在InvocationHandler的invoke方法中 回看ReflectiveFeign.newInsta…

怎么简单的把图片缩小?图片在线改大小的方法

在日常工作中经常需要在网上上传图片&#xff0c;但是一般网上不同的平台对上传的图片大小和尺寸都会有限定的要求&#xff0c;不符合要求无法正常上传使用。所以当遇到图片太大的问题时&#xff0c;该如何快速修改图片大小&#xff0c;有很多的小伙伴都很关注这个问题的解决方…

macOS上用Qt creator编译并跑shotcut

1 简介 Shotcut是一个开源的跨平台的视频编辑软件&#xff0c;支持WIN/MACOS/LINUX等平台&#xff0c;由于该项目的编译较为麻烦&#xff0c;踩坑几许&#xff0c;因此写此文章记录完整编译构建过程&#xff0c;后续按此法编译&#xff0c;可减少走弯路&#xff0c;提高生产力。…

Springboot项目打包:将依赖的jar包输出到指定目录

场景 公司要对springboot项目依赖的jar包进行升级&#xff0c;但是遇到一个问题&#xff0c;项目打包之后&#xff0c;没办法看到他里面依赖的jar包&#xff0c;版本到底是不是升上去了&#xff0c;没办法看到。 下面是项目打的jar包 我们通过反编译工具jdgui&#xff0c;来…

Compose Button移除水波纹效果

一、背景 在使用Compose实现Button按钮时&#xff0c;设计要求移除按钮的水波纹效果&#xff0c;只保留按压效果&#xff0c;经查Compose1.4.3版本中&#xff0c;并没有直接移除水波纹的能力 二、遇到问题 经过多次尝试&#xff0c;使用Compose的Button组件始终无法实现目标效…

SpringBoot基础篇

1&#xff1a;parent 目的&#xff1a;减少依赖配置 开发SpringBoot程序要继承spring-boot-starter-parentspring-boot-starter-parent中定义了若干个依赖管理继承parent模块可以避免多个依赖使用相同技术出现依赖版本冲突继承parent的形式也可以采用引入依赖的i形式实现效果…