一、引言
随着计算机硬件技术的飞速发展,尤其是多核CPU的普及,多线程编程已成为充分利用系统资源、提高程序并发性和响应速度的关键技术。
多线程编程允许一个程序中同时运行多个线程,每个线程可以独立地执行不同的任务。这种并行处理的方式能够显著减少程序的执行时间,提高程序的运行效率。同时,多线程编程还可以提升用户体验,因为多个线程可以同时处理不同的用户请求,使得系统能够更快地响应用户的操作。
在Linux系统中,线程得到了强大的支持。Linux内核为线程提供了丰富的功能和灵活的机制,使得开发者可以轻松地创建、管理和控制线程。Linux系统的线程模型基于POSIX线程(Pthreads)标准,该标准定义了一套用于创建、同步和管理线程的API,使得开发者可以跨平台地使用这些API来编写多线程程序。
Linux系统的线程具有以下几个特点:
- 线程轻量级:Linux线程的实现基于轻量级进程(LWP),相比于传统的进程,线程在创建和销毁时的开销更小,因此更适合用于实现高并发的应用程序。
- 共享内存空间:线程之间共享同一进程的地址空间,这使得线程之间的数据共享和通信变得非常简单和高效。
- 线程间通信与同步:Linux系统提供了多种线程间通信和同步的机制,如互斥锁、条件变量、信号量等,这些机制可以有效地协调线程之间的执行,确保程序的正确性和稳定性。
- 可移植性: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、线程的优缺点
线程相对于进程的优缺点,以及线程在并发编程中的应用场景。下面是详细解释:
优点
-
创建一个新线程的代价要比创建一个新进程小得多:
进程是系统分配资源的基本单位,它拥有独立的地址空间、数据栈、文件描述符等资源。因此,创建一个新进程需要分配和初始化这些资源,这通常是一个相对昂贵的操作。而线程是进程的执行单元,它共享进程的资源,因此创建新线程只需要在进程中分配一些必要的资源(如栈空间)即可,这通常比创建新进程要快得多。 -
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多:
进程切换时,操作系统需要保存当前进程的上下文(如程序计数器、寄存器值、内存管理等),然后加载目标进程的上下文。这个过程涉及到许多寄存器和内存数据的读写,因此开销较大。而线程切换时,由于线程共享进程的地址空间和其他资源,操作系统只需要保存和加载线程的少量上下文(如栈指针和程序计数器),因此开销较小。上下文切换的开销:进程切换需要保存和恢复更多的上下文信息,包括进程的程序计数器、寄存器状态、内存映射、I/O状态等。而线程切换只需要保存和恢复线程的上下文信息,由于线程共享同一进程的地址空间,所以线程的上下文信息相对较少。因此,线程切换的开销较小。
地址空间的切换:进程有独立的地址空间,进程切换时需要切换地址空间的映射关系,这涉及到页表的切换和TLB的刷新等操作,开销较大。而线程共享同一进程的地址空间,线程切换不涉及地址空间的切换,因此开销较小。
资源开销:由于进程间相互独立,切换两个进程需要保存和恢复更多的资源,包括地址空间、文件描述符等。而线程处于同一个进程内,它们共享进程的资源,因此线程切换的开销通常比进程切换小。
-
线程占用的资源要比进程少很多:
由于线程共享进程的地址空间和其他资源,因此每个线程只需要分配一些必要的资源(如栈空间)即可。这使得线程占用的资源比进程要少得多。 -
能充分利用多处理器的可并行数量:
多线程编程可以充分利用多处理器系统的并行处理能力。通过将计算任务分解为多个线程,可以让不同的处理器核心同时执行这些线程,从而加速程序的执行。 -
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务:
在I/O密集型应用中,线程可以在等待慢速I/O操作(如磁盘读写、网络通信等)完成时执行其他计算任务。这种并发执行方式可以显著提高程序的响应速度和吞吐量。 -
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现:
在计算密集型应用中,将计算任务分解为多个线程并在多处理器系统上并行执行可以显著提高程序的执行效率。通过将计算任务分配给不同的处理器核心,可以充分利用系统的计算能力。 -
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作:
在I/O密集型应用中,线程可以同时等待多个I/O操作的完成。当一个I/O操作阻塞时,线程可以切换到其他I/O操作或执行其他计算任务,从而避免了资源的浪费。这种重叠I/O操作的方式可以显著提高程序的性能。
缺点
-
性能损失:
- 同步和调度开销:当多个线程需要访问共享资源时,必须使用同步机制(如互斥锁、读写锁、条件变量等)来确保数据的一致性和正确性。这些同步机制会带来额外的开销,包括等待锁的释放、线程切换等。当计算密集型线程的数量超过可用的处理器核心数时,这些开销可能变得尤为显著。
- 线程创建和销毁:虽然线程的创建和销毁开销通常比进程小,但频繁地创建和销毁线程也会带来一定的性能损失。因此,在需要频繁创建和销毁线程的场景中,应该考虑使用线程池等技术来减少这种开销。
-
健壮性降低:
- 数据竞争:当多个线程同时访问和修改共享数据时,如果没有正确的同步机制,就可能导致数据竞争和不一致性。这种不一致性可能导致程序出现错误或不可预测的行为。
- 死锁和活锁:当多个线程相互等待对方释放资源时,就可能发生死锁。死锁会导致线程无法继续执行,从而影响程序的健壮性。活锁则是线程之间不断循环等待对方释放资源,但都没有成功,导致系统资源被无效占用。
-
缺乏访问控制:进程是访问控制的基本粒度,而线程则共享同一个进程的地址空间和资源。这意味着在一个线程中调用某些操作系统函数(如文件操作、网络通信等)可能会对整个进程造成影响。因此,在多线程编程中需要特别注意对共享资源的访问控制,以避免潜在的安全风险。
-
编程难度高:
- 复杂性增加:多线程编程需要考虑线程间的同步、通信、死锁等问题,这使得程序的逻辑变得更加复杂。
- 调试困难:多线程程序中的错误往往难以定位和调试,因为线程间的执行顺序和状态可能随时发生变化。
三、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_IGN
、SIG_DFL
或者自定义的信号处理函数)。
Linux线程与进程的联系主要体现在以下几个方面:
- 共享资源:
- 线程是进程中的一条执行流,因此它们共享其所属进程的大部分资源。这些共享的资源包括地址空间、文件描述符、信号处理器等。
- 进程是资源分配的基本单位,每个进程都拥有独立的地址空间和其他系统资源。然而,当线程在进程中创建时,它们会共享这些资源。
- 调度:
- 进程和线程都可以被系统调度以在不同的时间点上执行。不过,由于线程共享进程的资源,因此线程的切换通常比进程的切换更加高效。
- 在Linux中,线程的实现是通过轻量级进程来完成的,这使得线程在内核中的调度与进程类似。
- 并发执行:
- 进程和线程都可以实现并发执行。多个进程可以同时运行,而在同一个进程内部,多个线程也可以并发执行。
- 由于线程共享进程的地址空间,因此它们之间的通信和同步通常比进程之间的通信和同步更加高效。
在多线程环境中,每个线程都拥有一些私有的资源,以确保它们能够独立且并发地运行:
- 线程的硬件资源(CPU寄存器的值)(调度):
- CPU寄存器是CPU内部的存储单元,用于存储指令执行过程中产生的数据。在多线程环境中,由于多个线程可能同时运行在CPU上,因此每个线程都需要有自己的寄存器集合来保存其执行过程中的状态和数据。这样,当线程被调度执行时,它可以恢复其之前的状态并从上次中断的位置继续执行。
- 当从一个线程切换到另一个线程时,操作系统会保存当前线程的寄存器状态,并加载下一个要执行的线程的寄存器状态。这个过程确保了每个线程都能够在其自己的上下文中运行,而不会受到其他线程的影响。
- 线程的独立栈结构(常规运行):
- 栈是一种后进先出(LIFO)的数据结构,用于存储线程执行过程中产生的局部变量、方法调用等信息。每个线程都有自己的独立栈,用于保存其执行历史和状态。
- 当线程调用一个方法时,会在栈上为该方法分配一个栈帧,用于存储该方法的局部变量和操作数等信息。当方法执行完毕后,其对应的栈帧会被弹出栈,释放占用的内存空间。
- 线程的独立栈结构确保了每个线程都能够在其自己的内存空间中执行,而不会干扰其他线程的执行。同时,它也为线程之间的数据隔离提供了支持。
📓线程独有的资源:线程ID,寄存器内容,栈,线程局部存储(TLS),信号屏蔽字,调度优先级 ,errno。
下面我们来具体谈一谈线程和进程的区别:
- 资源占用:
- 进程:进程是系统分配资源的基本单位。每个进程都拥有独立的内存空间、系统资源(如文件描述符、信号处理器等)和独立的执行环境(包括程序计数器、堆栈和一组系统寄存器)。
- 线程:线程是进程的一个执行单元,共享进程所拥有的资源(如内存空间、文件描述符等),但每个线程有自己的栈结构和线程控制块。因此,线程相对于进程来说,资源占用更少,创建和销毁的开销也更小。
- 调度和切换:
- 进程:由于进程拥有独立的内存空间和系统资源,因此进程之间的切换需要保存和恢复更多的上下文信息,这导致了进程切换的开销相对较大。需要切换地址空间和页表。
- 线程:线程之间的切换只需要保存和恢复线程的上下文信息(如程序计数器、堆栈等),而不需要切换整个进程的上下文,因此线程切换的开销相对较小。不需要切换地址空间和页表。
- 通信和同步:
- 进程:进程之间的通信通常需要通过操作系统提供的进程间通信(IPC)机制来实现,如管道、消息队列、信号量、共享内存等。这些机制的实现相对复杂,且开销较大。
- 线程:由于线程共享进程的内存空间,因此线程之间的通信和同步相对简单。线程可以通过全局变量等方式进行通信,也可以通过互斥锁、条件变量等同步机制来协调线程的执行。
- 独立性:
- 进程:进程具有独立性,一个进程的崩溃不会影响其他进程的执行。同时,进程之间的隔离性也保证了系统的安全性。
- 线程:线程属于进程的一部分,一个线程的崩溃可能导致整个进程的崩溃。此外,由于线程共享进程的内存空间,因此线程之间的错误可能会相互影响。
- 系统开销:
- 进程:由于进程拥有独立的资源,因此创建和销毁进程的开销相对较大。同时,进程之间的切换也需要保存和恢复更多的上下文信息,导致系统开销增加。
- 线程:线程的创建和销毁开销较小,且线程之间的切换开销也较小。这使得线程在需要频繁创建和销毁执行单元的场景中具有优势。
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中用于标识线程和进程的机制:
- LWP:LWP是线程在Linux中的一种表示方式,通常用于在工具(如
ps
命令)中标识线程。每个线程都有一个唯一的LWP ID,这个ID在进程内部是唯一的,但在整个系统中可能不是唯一的(因为不同的进程可以有相同LWP ID的线程)。LWP ID通常用于在调试和性能分析时标识和区分线程。 - 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()
函数用于创建一个新的线程。这个函数允许在多线程程序中添加并行执行的代码路径。
参数设置:
- pthread_t *thread:这是一个指向
pthread_t
类型的指针,用于存储新创建线程的标识符。pthread_t
是一个不透明的数据类型,用于唯一标识一个线程,是一个输出型参数。 - const pthread_attr_t *attr:这是一个指向线程属性对象的指针,用于设置线程的属性,如栈大小、调度策略等。如果不需要设置特定的属性,可以传递
NULL
,表示使用默认属性。 - void *(*start_routine) (void *):这是新线程开始执行时调用的函数,即线程的入口点。这个函数应该返回一个
void *
类型的指针,通常用于传递线程执行的结果给主线程或其他线程。该函数的参数是一个void *
类型的指针,用于向线程函数传递参数。 - 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)来管理的,这些库为用户提供了创建、管理和同步线程的高级接口。
- 轻量级进程(LWP):在Linux内核中,线程是通过轻量级进程来实现的。这些LWP与常规进程(由
fork
创建)在内核中的表示非常相似,但LWP与创建它的进程共享相同的地址空间和某些其他资源。 - 线程库(如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系统中变得更加灵活和高效。
在通过库函数创建线程时,操作系统会执行以下步骤来找到和管理这些线程:
- 库函数调用:首先,程序会调用库函数(如
pthread_create
)来请求创建一个新线程。 - 封装
clone
调用:库函数内部会封装对clone
系统调用的调用。clone
系统调用允许程序指定要共享哪些资源,以及新线程的开始执行点。 - 设置线程属性:在调用
clone
之前,库函数会根据提供的线程属性(如栈大小、优先级等)来设置相关参数。 - 执行
clone
调用:库函数会执行clone
系统调用,传递必要的参数。操作系统内核会处理这个调用,并创建一个新的线程。 - 分配资源:操作系统内核为新线程分配必要的资源,如内存空间、栈等。这些资源可能是从现有的共享资源中分配出来的,也可能是为新线程单独分配的。
- 将新线程加入调度队列:一旦新线程的资源被分配并设置好,操作系统会将其加入到调度队列中,等待调度器选择执行。
- 线程调度和执行:调度器会根据一定的算法从调度队列中选择一个线程来执行。当调度器选择到新创建的线程时,它会开始执行线程的代码。
通过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: Successzyb@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_func1
和 thread_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、线程的终止
- 正常退出
当线程完成了其任务并正常退出时,它可以通过以下几种方式来实现:
-
函数返回:线程执行的函数(通常是
pthread_create
指定的函数)执行完毕并返回时,线程将正常退出。 -
调用
pthread_exit()
:线程可以在任何时候调用pthread_exit()
函数来立即退出。该函数接受一个指向void
的指针作为参数,该指针可以被其他线程通过pthread_join()
函数获取。 -
主线程返回:如果主线程(即调用
pthread_create()
创建其他线程的线程)执行完毕并返回,那么整个进程(包括所有线程)将终止。但是,其他线程在此之前应该已经正常退出或被终止。
- 异常退出
线程也可能由于异常或错误而退出,这些异常或错误通常是由于编程错误或不可预测的运行时错误引起的。
-
未捕获的异常:当线程遇到无法恢复的异常(如除以零、野指针访问等)时,操作系统通常会向进程发送一个信号(如
SIGSEGV
、SIGFPE
等)。如果进程没有安装信号处理器来捕获这些信号,或者信号处理器没有适当地处理它们,那么整个进程可能会被终止。 -
调用
pthread_cancel()
:虽然这不是真正的“异常”退出,但pthread_cancel()
函数允许一个线程请求另一个线程终止其执行。被请求的线程可以选择立即终止,或者在达到某个取消点(cancellation point)时终止。取消点通常是某些库函数调用时的点,在这些点上,线程会检查是否有取消请求。
处理线程异常退出的策略
- 设置信号处理器:对于可能导致进程终止的信号,如
SIGSEGV
、SIGFPE
等,可以设置信号处理器来捕获这些信号并尝试恢复或优雅地终止进程。然而,由于线程共享相同的地址空间,处理这些信号可能会很复杂。- 使用线程取消状态处理程序:如果使用了
pthread_cancel()
来请求线程终止,可以设置一个取消状态处理程序(cancellation handler)来处理取消请求。这个处理程序可以在线程响应取消请求之前执行一些清理工作。- 日志和监控:在程序中实现日志记录和监控机制,以便在出现异常时能够及时发现并解决问题。
需要注意的是,虽然线程是进程的执行单元,并且它们共享同一个地址空间,但线程的异常退出并不一定总是导致整个进程的终止。这取决于操作系统、信号处理器的设置以及异常的性质和处理方式。
然而,在多线程环境中,一个线程的异常退出通常会对整个进程的状态和行为产生重大影响,因此需要谨慎处理。
这还是因为所有线程共享同一个进程地址空间,且操作系统通常将进程视为一个整体来处理,所以当进程收到一个致命信号时,它会终止整个进程,包括进程内的所有线程。这是因为操作系统通常无法安全地只终止一个线程而不影响其他线程的状态和数据。
简单来说,线程退出分为三种情况:
- 代码跑完,结果对:如果线程正常执行完毕,并且没有遇到任何问题,那么它就可以正常退出。线程的退出并不会导致整个进程的终止,除非这是进程中的最后一个线程。
- 代码跑完,结果不对:如果线程的代码执行完毕但结果不正确,这通常是由于逻辑错误、数据竞争、未同步的访问或其他并发问题导致的。这种情况不会直接导致进程终止,但可能会导致程序行为异常或数据损坏。
- 出异常了:当一个线程遇到无法恢复的异常时(如除以零、野指针访问等),操作系统通常会向进程发送一个信号(如
SIGSEGV
或SIGFPE
)。默认情况下,这些信号会导致进程终止。因为线程共享进程的地址空间,所以一个线程中的异常通常会导致整个进程的终止。
关于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_cancel
和pthread_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
函数)。当线程被设置为分离状态时,系统会在线程结束时自动释放线程所占用的资源,而不需要其他线程来回收这些资源。
如何理解线程分离: 分离只是线程的工作状态,底层依旧属于同一个进程。分离仅仅是不需要等待了。
-
资源回收:在POSIX线程(pthreads)中,当一个线程结束时,它所占用的栈空间和其他资源并不会立即被释放,除非有另一个线程调用
pthread_join
来回收这些资源。如果线程被设置为分离状态,那么当线程结束时,这些资源会自动被系统回收,而不需要其他线程介入。 -
主线程不关心:当主线程(或其他线程)创建一个新线程并设置其为分离状态时,主线程就不再需要关心这个新线程的执行结果和结束时间。也就是说,主线程不需要调用
pthread_join
来等待新线程结束。 -
join函数的行为:如果一个线程被设置为分离状态,并且你尝试对它调用
pthread_join
,那么pthread_join
会返回错误(通常是EINVAL
)。这是因为分离状态的线程不允许被其他线程join。 -
底层仍属于同一进程:尽管线程被设置为分离状态,但它仍然是创建它的进程的一部分。这意味着线程可以访问和修改该进程的共享内存区域,并且可以访问该进程打开的文件描述符等。
-
不需要等待:这是线程分离最直观的特点。一旦线程被设置为分离状态,你就不需要(也不能)等待它结束。这可以提高程序的并发性和响应性,但也可能增加程序管理的复杂性,特别是当多个线程需要协调其活动时。
如果尝试对一个已经分离的线程调用pthread_join
,pthread_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++中的vector
、string
等不能存储。
#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
。