泉州免费建站,做网站公司 衡阳公司,wordpress另一更新正在运行,百度排行榜风云榜一、引言
随着计算机硬件技术的飞速发展#xff0c;尤其是多核CPU的普及#xff0c;多线程编程已成为充分利用系统资源、提高程序并发性和响应速度的关键技术。
多线程编程允许一个程序中同时运行多个线程#xff0c;每个线程可以独立地执行不同的任务。这种并行处理的方式…一、引言
随着计算机硬件技术的飞速发展尤其是多核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系统中线程的实现基于轻量级进程LWPLightweight Process或内核线程的概念。尽管线程与进程共享相同的地址空间但Linux内核为每个线程都维护了一个独立的task_struct结构体用于表示线程的状态和相关信息。这使得Linux能够像管理进程一样管理线程包括调度、优先级设置、同步等。
Linux内核实现线程的方式主要是通过共享进程地址空间的一组线程来完成的。在Linux中线程也称为轻量级进程LWPLightweight Process。每个线程都有一个唯一的线程IDTID和一个相关的task_struct结构但所有线程共享同一进程的地址空间包括代码段、数据段、堆和栈等。
关于LWP和PIDProcess ID进程ID这是Linux中用于标识线程和进程的机制
LWPLWP是线程在Linux中的一种表示方式通常用于在工具如ps命令中标识线程。每个线程都有一个唯一的LWP ID这个ID在进程内部是唯一的但在整个系统中可能不是唯一的因为不同的进程可以有相同LWP ID的线程。LWP ID通常用于在调试和性能分析时标识和区分线程。PIDPID是进程的唯一标识符它在整个系统中是唯一的。一个进程的所有线程共享同一个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_castchar *(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;
}输出结果 zybmyserver:~/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 zybmyserver:~/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中mmapMemory Map是一个系统调用它允许程序将一个文件或设备的一部分或其他对象映射进内存。但是在创建线程上下文中mmap通常被用于动态地分配内存区域特别是为线程栈分配内存。 当Linux内核创建一个新线程时它并不总是从进程的堆或数据段中分配栈空间。相反它可能会使用mmap系统调用来请求一个私有的、匿名的内存区域该区域将用作新线程的栈。这种方法的优点是它允许内核更直接地管理栈内存并可能提供更好的性能和隔离性。 总的来说mmap是一个强大的系统调用它允许程序以灵活的方式管理内存。在创建线程时它可能被用作一种机制来分配和管理线程栈。 下面我们来证明线程有独立栈结构 无论是单线程程序还是多线程程序每次函数被调用时都会在其调用栈上创建一个新的栈帧Stack Frame。这个栈帧包含了函数调用的所有信息比如函数的返回地址、传递给函数的参数以及函数内部的局部变量。 #include pthread.h
#include iostream
#include unistd.h
#include cerrno
#include cstringstd::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_castchar *(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;
}运行结果 zybmyserver:~/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.hint g_val 100; // 全局变量被共享void *thread_func1(void *arg)
{std::string name static_castchar *(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_castchar *(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;
}zybmyserver:~/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 stringconst 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_castThreadData *(args);Result *res new Result(td-run(), td-threadname());delete td;sleep(2);return res;
}int main()
{std::vectorpthread_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::vectorResult * 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*。
代码中创建了一个 vectorpthread_t 来存储线程ID循环创建多个线程每个线程都执行 handerTask 函数并传递一个 ThreadData 对象作为参数。使用 pthread_join 等待每个线程完成并将返回的结果Result*存储在 vectorResult* 中。使用了 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线程pthreadsAPI中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.hvoid *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;
}输出结果 zybmyserver:~/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_joinpthread_join函数将返回错误EINVAL表示无效的参数但并不会直接导致进程退出。它只是告诉调用者该线程已经被分离不能通过pthread_join来等待。这是调用pthread_join时可能遇到的错误处理的一个例子
#include iostream
#include unistd.h
#include pthread.h
#include errno.hvoid *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;
}zybmyserver:~/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这是C11标准中的关键字是一个存储类修饰符它告诉编译器这个变量是线程局部的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_castchar *(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_castchar *(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标准的一部分。从C11开始标准库提供了 thread_local 关键字作为线程局部存储的官方支持。因此如果正在编写可移植的代码建议使用 thread_local 而不是 __thread。