深入剖析pthread并发编程与线程基本元素与状态 在本文中,我们将探讨pthread并发编程的基础,特别是线程的基本概念和进程属性。我们将深入了解线程在内存布局的形式,并帮助您理解线程的深刻内涵。深入理解pthread_create与基础例子介绍 在解析pthread_create之前,我们通过一个简单的例子来了解pthread。我们将创建...
Pthread 并发编程一——深入剖析线程基本元素和状态
深入剖析pthread并发编程与线程基本元素与状态
在本文中,我们将探讨pthread并发编程的基础,特别是线程的基本概念和进程属性。我们将深入了解线程在内存布局的形式,并帮助您理解线程的深刻内涵。
深入理解pthread_create与基础例子介绍
在解析pthread_create之前,我们通过一个简单的例子来了解pthread。我们将创建一个线程并打印"Hello world"字符串。
接下来,我们编译上述程序。程序首先定义一个线程,然后创建线程并执行func函数。当创建完成后,主线程使用pthread_join阻塞自己,直到等待线程t执行完成,主线程才会继续。
现在,让我们详细分析pthread_create的函数签名和参数。
深入理解参数thread
我们使用pthread_self获取线程ID,并通过保存线程ID地址的变量t获取线程ID。比较这两个获取的结果。
程序执行结果如下图所示。
从结果中,我们了解到变量pthread_t t保存的是线程ID地址。参数t与线程ID之间的关系如下所示:
程序首先对t取地址,然后将其转化为long类型的指针,解引用即可得到对应地址的值,即线程ID。
深入理解参数arg
我们定义一个结构体用于保存字符信息,并创建一个结构体对象,将对象指针作为参数传递给要执行的线程函数,并在内部打印字符串内容。
程序执行结果如下所示。
可以看到,函数参数已经正确传递。
深入理解参数attr
在深入解析参数attr之前,我们了解程序内存布局。在64位操作系统下,程序虚拟内存布局大致如下,从下至上依次为:只读数据/代码区、可读可写数据段、堆区、共享库映射区、程序栈区及内核内存区域。程序执行区域在栈区。
根据内存布局示意图,简化得到单个线程执行流和大致内存布局如下(程序执行时有其栈帧及寄存器现场,图中已标识寄存器)。
当进行函数调用时,函数栈帧会从上往下生长。我们进行测试,观察栈帧最大容量。
程序执行结果如下图所示。
从结果中,我们得知在第8次申请栈内存时遇到段错误,因此判断栈空间大小约为8MB。事实上,Linux操作系统中程序栈空间默认最大为8MB。
现在,我们测试创建线程时栈大小。
程序执行结果如下图所示,创建线程时栈最大大小为8MB。
设置线程栈空间大小
如果需要大于8MB的栈空间,该如何操作?这就是我们讨论的attr。attr是一个pthread_attr_t对象,主要用于设置线程属性,包括栈大小。下面的程序将栈空间大小设置为24MB,并进行测试。
程序执行结果如下图所示。
结果表明,设置的24MB栈空间大小有效,通过递归次数可以看出确实申请了如此大的空间。程序中对属性的操作如下,也是属性操作的一般流程:
自己为线程栈申请空间
通过函数pthread_attr_setstacksize设置栈空间大小后,使用程序验证新设置的栈空间。本节将介绍如何使用自定义内存空间作为线程栈。我们将使用两种方法验证这一点:
使用malloc函数申请内存空间
程序执行结果如下图所示。
结果显示,设置的栈空间大小为2MB成功了。程序主要使用pthread_attr_setstack函数设置栈低地址和空间大小。申请的内存空间内存布局如下图所示:
使用mmap系统调用在共享库空间申请内存作为栈空间
程序使用mmap系统调用在共享库空间申请内存作为栈空间,程序执行结果与前面不同,只是在申请内存方面有所变化,总体方向不变。
多个线程共享进程虚拟地址空间,我们只需为每个线程提供栈空间即可。基于此,多个线程执行流和内存布局如下图所示。
图中展示了不同线程拥有各自的栈空间和寄存器现场,栈空间可以位于堆区或共享库映射区域。
深入理解线程状态
在pthread中,我们提供了函数pthread_cancel取消正在执行的线程。取消后,线程退出状态设置为宏定义PTHREAD_CANCELED。我们通过例子理解线程取消的过程。
程序在主线程中使用pthread_cancel取消线程执行,编译执行程序成功,程序正确执行且通过assert。我们首先不深入分析代码执行流程和函数意义。需要了解线程的基本特性。
与线程取消执行相关的属性有两个:
通常我们不会使用PTHREAD_CANCEL_ASYNCHRONOUS,因为这可能导致线程资源未释放,给系统带来灾难,如malloc申请的内存空间未释放、锁和信号量未释放,尤其是锁和信号量未释放,容易导致死锁。
有了知识基础,我们可以讨论函数细节:
创建默认状态和类型的线程执行死循环,观察是否能被取消:
程序启动线程进行死循环,程序不会终止,因为主线程等待线程结束,但线程进行死循环,且未调用取消点函数,程序不会终止取消。
更改程序,将线程取消类型设置为PTHREAD_CANCEL_ASYNCHRONOUS,再查看程序执行结果。
程序中在线程执行函数中使用pthread_setcanceltype将取消类型设置为PTHREAD_CANCEL_ASYNCHRONOUS,这样其他线程使用pthread_cancel即可立即取消线程执行。
pthread_setcanceltype函数签名与前面类似,参数含义相同。type表示设置的取消状态,有两个参数:PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DISABLE。oldstate表示原始线程取消状态,传入int类型指针将保存状态。
关于线程的细节还有较多内容,本文主要介绍核心内容。
栈大小程序的一个小疑惑
使用小程序测试线程栈空间大小,并打印func函数调用次数。每次调用时申请1MB栈空间变量。下面两个程序只有func函数有差异,主要区别在于:
程序输出结果一致,只打印times = 7。为什么即使后申请内存空间,打印times = 8时也只打印到7?
问题原因在于编译器在编译后的程序中如何申请内存空间。显示了func函数的汇编代码。
汇编代码显示编译器在函数开始时就申请了对应空间,因此程序被编译成汇编指令后,函数func在开始时就申请了空间,导致了结果。
总结
本文介绍了线程的基本元素和状态,重点讲解了与线程相关的属性函数,使用的主要函数如下。希望对您有所帮助!
更多内容合集可访问:github.com/Chang-LeHung...2024-08-20