ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【Linux系统】线程的基本概念及使用

【Linux系统】线程的基本概念及使用 目录一、什么是线程如何理解1. 从进程到线程的理解2. Linux中的实现方案3. 怎么理解线程分配资源4. 线程VS进程二、线程的使用/控制1. 环境说明2. 创建线程pthread_create函数解析进程的PID和线程的LWP3. 等待线程pthread_join函数解释第二个参数 retval 怎么使用4. 线程退出pthread_exit5. 线程取消pthread_cancel三、线程的 tid 的深入理解四、多线程的使用多线程的创建线程的栈线程的局部存储五、线程分离一、什么是线程如何理解1. 从进程到线程的理解我们知道一个进程一定有自己的一个内核数据结构PCB / task_struct一个程序地址空间还有一个页表来映射物理地址。所以我们创建一个进程就是创建一个 task_struct 并分配一套独立的地址空间。此时进程既是资源分配的单位也是 CPU 调度的单位。但是如果在一个进程中只创建一个新的 task_struct但不为它申请新的地址空间而是让它共享已有进程的地址空间会发生什么此时这个新的 task_struct 依然拥有独立的 CPU 上下文可以独立被调度、运行原本进程中的一部分代码但它没有独立的资源而是直接访问那个共享的地址空间代码、数据、堆。这个新的执行分支执行流就是我们所说的线程。在一个进程中我们如果像这样创建很多个执行流那么我们就可以创建出很多个线程了在Linux中线程与进程的结构关系图下由此可以得出两点在一个进程中每一个 task_struct 都是一个执行流即线程 但它们右侧指向的却是同一个 mm_struct而地址空间是进程访问资源的窗口所以线程都是通过同一个地址空间在进程内部执行的具体来是就是线程是在地址空间内运行的。CPU 调度的基本单位是线程task_struct。CPU 并不直接调度“进程”这个概念它只关心把时间片分给哪个 task_struct 去执行线程执行的代码只是进程的一部分所以线程的执行粒度要比进程更细。所以我们可以给出一个定义线程是进程内的一个执行分支线程的执行粒度要比进程更细。在 Linux 中线程的本质上就是 Linux 中的执行流。任何执行流要执行都必须要有资源而地址空间就是进程提供的资源窗口。线程必须依附于进程地址空间才能运行进程内部可以包含一个或多个线程但 CPU 调度时只看 task_struct 。所以我们认为线程是操作系统调度的基本单位。既然如此那么我们该如何理解进程呢之前我们说 进程内核数据结构代码和数据。而现在我们则认为进程是资源分配的基本实体。也就是在操作系统内分配资源的方式是按照进程为单位进程分配的。当我们创建一个进程时操作系统会为其分配独立的虚拟地址空间、页表、文件描述符表以及 I/O 设备等资源这些资源构成了进程运行的“环境”。当我们每创建一个线程就是在这个分配的资源中创建一个task_struct执行流资源同时和地址空间建立关系它属于这个进程的内部。所以什么是进程如图所以一个执行流就是一个线程进程和线程是 1 : n 的关系。在CPU的角度来说不关心什么是进程还是线程的概念CPU只有调度执行流的概念所以CPU看到它们的数量关系是线程 执行流 进程 。结论进程是承担分配系统资源的基本实体。线程是 CPU 调度的基本单位它是进程内部的执行流资源。2. Linux中的实现方案上面的图中表示的都是Linux中的线程结构。但是不同的操作系统虽然线程概念是一样的但是它们的具体实现却是不一样的。操作系统要对线程进行管理先描述再组织。在大部分的操作系统中通常有专门的内核数据结构来描述线程例如在Windows中线程是通过数据结构 struct tcb 来描述和组织的而进程则是PCB来管理的。进程维护了内存、文件等资源线程仅保留私有栈和寄存器其余通过指针共享进程资源。这种设计虽然清晰但也带来了维护成本代码重复维护、管理、排查问题成本就高。而 Linux 的实现方案则完全不同。Linux 内核中并没有名为线程的独立数据结构它是极其巧妙地复用了 task_struct 来模拟了线程也就是上面第一张图所示。因为复用了进程的数据结构和管理算法Linux 中的线程也被称为轻量级进程。它们看起来像进程都有 task_struct但因为它们共享地址空间所以创建和切换的开销远小于传统进程。所以Linux没有真正意义上的线程即没有专门没收到结构体而是使用 “ 进程 ” 进程的内核数据结构模拟线程。3. 怎么理解线程分配资源本质当我们说“创建一个线程并分配资源”时操作系统并不会直接把物理内存条切一块给线程而是在这个虚拟空间里做区域划分主要是划分栈区的空间因为其他区域都是共享的。对于栈空间操作系统会在虚拟空间中为每个线程预留一块独立的区域只要在这个范围内读写不越界线程就认为自己拥有了这块资源。对于代码和数据在所有的线程在各自的视角里看到的都是同一段虚拟地址范围。所以线程目前分配到的资源本质上就是它在虚拟地址空间中划定的一段范围。页表的结构虚拟转物理的逻辑Linux 使用 task_struct 模拟线程多个线程共享同一个 mm_struct但当 CPU 真正开始执行指令时它只认物理地址。如果只给了线程一个虚拟地址范围而没有对应的物理内存程序一运行就会报错缺页异常所以就有了页表。注意在32位操作系统中内存通常被切分为4KB2^12 大小的页。这意味着整个空间被划分成了 2^20 约100万个页面。之前我们也谈过页表是用来让虚拟地址和物理地址建立映射关系的一个表其中除了有虚拟地址和物理地址的关系还有一些标志位比如权限等等。但是页表其实并不是一个真正的表格但是页表其实并不是一个真正的、完整的“表格”。为什么这么说32位系统拥有 2^32 即4GB的虚拟地址空间如果每一个地址都要映射我们就需要一张有2^32行的表。假设表中每一行只有虚拟地址和物理地址一个地址要32个比特位(4字节)那么每一行有两个地址就占用8个字节那么这张表的总大小就是2^32(行) * 8(字节) ≈ 32GB 内存消耗就会很大。因此Linux 采用了多级页表的设计虚拟地址有32位可以将它拆分为10个一组10个一组12个一组一共三组。这样就将虚拟地址通过一个页目录二级页表映射到物理地址上了具体结构如下示意图所示其中页目录中的每一个项称为页目录表项二级页表中的每一项称为页表表项。而页表的其他字段主要是在二级页表中维护的。如果我们有一个虚拟地址则首先将它拆分为10个一组10个一组12个一组。然后通过一下步骤它的前十个比特位相当于是页目录中下标一个前10个比特位就可以找到一个二级页表的指针。中间的10个比特位相当于是二级页表的下标通过这10个比特就可以得到一个物理内存的页框的起始地址。所以通过前20位就找到了一个物理内存的页框的起始地址最后12位表示的是页框的起始地址的偏移量。最终我们得到一个物理内存的页框的起始地址和一个12位的偏移量此时将两者相加就得到一个实际的物理地址。这就是虚拟地址到物理地址的映射。那么这样的结果需要多少个空间呢一级页表(页目录)需要 2^10(个) * 4(字节) 4KB二级页表有1024个每一个二级页表要存 2^10 个地址所以二级页表的总大小就是 2^10(一个二级页表中的地址数目) * 4 (字节) * 2^10(二级页表的数目) 4MB。所以整个页表就需要大约4MB的空间。并且因为一个进程不会讲整个地址空间都用完所以二级页表的数目就是小于2^10个的空间更小。所以缺页中断是什么其实就是 CPU 拿着虚拟地址去查页表时发现对应的页表项中有效位Present Bit为 0。这意味着该页面虽然合法但当前并不在物理内存中这就发生缺页异常CPU 会把导致异常的那个虚拟地址存在 CR2 寄存器里然后转去执行操作系统的异常处理程序。操作系统读取 CR2 中的地址找到对应的数据通常从磁盘 Swap 区加载将其调入物理内存并更新页表最后让程序重新执行刚才那条指令。4. 线程VS进程概念进程是系统进行资源分配的基本单位。它拥有独立的地址空间、文件描述符等资源。线程是系统进行调度的基本单位CPU 调度的最小单元。它是进程内的一个执行流共享进程的资源。为什么线程比进程更轻量化创建与销毁的代价小。创建一个新进程需要分配独立的内存空间建立页表映射复制父进程的资源开销巨大创建一个新线程只需要分配少量的栈空间和寄存器上下文它直接复用所属进程的mm_struct不需要建立新的页表映射。切换与运行的开销低。进程切换需要切换虚拟内存空间页表寄存器内容进程上下文Cache 中的原本的数据会失效需要重新加载数据同时需要操作系统做大量工作来保存和恢复环境线程切换只需保存和恢复少量寄存器不需要刷新缓存 Cache 中的数据依然有效效率极高。补充Cache 是位于 CPU 和主内存之间的一种硬件级别的高速存储器。它的速度比内存快得多但容量较小。它的作用是暂存 CPU 即将用到的指令和数据也就是缓存的热数据。如果切换进程导致 Cache 失效CPU 就必须去慢速的内存里找数据程序就会变卡。但是切换线程其中数据仍然有效无需重新加载。通过cat \proc\cpuinfo 可以查看到它的大小两个概念计算密集型应用通过将计算任务分解到多个线程中可以充分利用多处理器的并行计算能力显著提高执行效率。如果是多核 CPU线程数通常设置为等于 CPU 核心数时效率最高如果只有一个CPU单线程单执行流是最快的因为不用切换。I/O密集型应用当等待慢速 I/O 操作如网络请求、磁盘读写时程序可以利用空闲时间执行其他计算任务。那么我们就可以通过将 I/O 操作重叠线程可以同时等待不同的 I/O 事件。这种类型多线程最快的比如一边写代码一边下载开发工具就是多线程此时线程数通常远大于 CPU 核心数。线程缺点缺乏访问控制在一个线程中调用某些操作系统函数可能会对整个进程造成影响会导致健壮性降低所以线程间缺乏像进程那样的隔离保护。编程难度提高因为线程共享代码和数据所以编写与调试一个多线程程序比单线程程序困难得多开发者必须处理死锁、活锁、数据竞争等并发问题。注意如果单个线程如果出现异常它所在的整个进程也会随着崩溃。因为线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该进程内的所有线程也就随即退出。线程的资源线程虽然共享进程的大部分数据但也拥有自己独立的一部分数据以维持执行流私有数据线程 ID、一组寄存器值、栈Stack、errno 变量、信号屏蔽字、调度优先级。共享数据进程的多个线程共享同一地址空间因此 Text Segment代码段、Data Segment数据段都是共享的。这意味着定义的函数和全局变量在各线程中都可以访问。除了进程地址空间各个线程间还共享以下进程资源和环境文件描述符表每种信号的处理方式SIG_IGN、SIG_DFL或者自定义的信号处理函数当前工作目录用户 ID 和组 ID二、线程的使用/控制1. 环境说明Linux线程没有明确的线程但是提供了轻量级进程的概念这就注定了Linux不会直接通过线程的系统调用只会提供轻量级进程的系统调用。但是用户需要线程的接口所以Linux程序员就在应用层开发了pthreadPOSIX Threads库它对底层的轻量级进程接口进行了封装为我们提供了一套简洁、标准的线程操作接口如创建、销毁、同步等。目前几乎所有的 Linux 发行版都默认自带了 pthread 库。因此在 Linux 下进行多线程编程实际上就是调用 pthread 库中的函数。这种pthread库我们称为原生线程库虽然系统默认自带该库但 pthread 属于独立的外部库不属于 C 语言标准库的核心部分所以在使用 gcc/g 编译包含多线程代码的 C/C 程序时必须显式地链接该库也就是在编译指令后面加上:-lpthread否则会出现错误。2. 创建线程pthread_createpthread_create。它用于在进程中创建一个新的执行流线程。它不是系统调用。函数解析在使用该函数前需要包含头文件 pthread.h。#include pthread.h int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);参数说明thread pthread_t * 类型这是一个输出型参数。当线程创建成功后新线程的 ID 会被写入这个变量中。后续我们就是通过这个 ID 来操作该线程的。attrconst pthread_attr_t * 类型用于设置线程的属性如栈大小、分离状态等。通常情况下我们不需要修改默认属性直接传入 nullptr (或 NULL) 即可。start_routine这是一个函数指针指向新线程启动后要执行的代码函数这个函数只有一个参数 void * 的参数在使用时必须对其做强转类型。返回值也是 void *。arg void * 类型这个参数是传递给线程函数的。当线程创建成功后新线程回调线程函数的时候传递的参数。返回值成功返回0失败直接返回错误码它不像系统调用那样设置 errno。注意void* 可以指向任意类型的数据如 int、char、结构体等但在解引用访问数据之前必须将其强制转换为具体的类型。由于 pthread 库不是 C 标准库的一部分编译时必须显式链接即带上 -lpthread 。所以这个函数不是系统调用。代码示例#include iostream #include pthread.h #include unistd.h void *threadRun(void* args) { while(1) { std::cout new thread: getpid() std::endl; sleep(1); } return nullptr; } int main() { pthread_t tid; pthread_create(tid, nullptr, threadRun, nullptr); while(1) { std::cout main thread: getpid() std::endl; sleep(1); } }在这个代码中只创建了一个线程它执行的代码是threadRun函数中的代码而主线程执行的则是main函数。此时这两个线程会同时执行其中的死循环打印信息运行结果如图进程的PID和线程的LWP·通过上面代码的运行结果可以看出它确实是两个执行流。如果我们 ps axj 查看时则可以看到是一个进程如图通过上面代码可以看到主线程和子线程打印的PID都是一样的那么为什么不同的执行流通过getpid() 拿到的是同一个值呢这是因为虽然一个进程中可以有多个线程即多个执行流但getpid() 获取的是一个进程的 ID而不是执行流的 ID。一个进程中不同的执行流虽然 PID 相同但内核依然需要区分它们来进行 CPU 调度。这就引出了 LWP 轻量级进程的概念。 我们可以通过ps 的-L选项来查看当前用户启动的轻量级进程一般是ps -aL来显示详细信息如图(因为每一次运行程序它的 pid 数值可能不一样)其中PID的值就是进程的PID线程的PID相同就表示它们属于同一个进程而LWP才是标识线程的唯一标识符即轻量级线程的PID。1它LWP是操作系统层的概念操作系统调度看的就是它。2之前说我们在pthread_create函数第一个参数也是线程的ID这个我们叫它tid在后面详谈但这个函数第一个参数的表示的ID其实是给用户使用的用户层使用的。如果我们观察可以发现它们的值是不同的但是都可以实现线程唯一标识只不过是在不同层面的罢了。如果LWP和PID相同的一个线程就是主线程线程也有主次即第一个被创建的线程子线程就是主线程通过 pthread_create 创建的如果LWP和PID不相同则就表示它是子线程有主线程创建出来的。kill 杀掉线程的影响通过 kill -9 加上线程的LWP可以杀掉对应线程同时整个进程也会被杀掉。因为向任意线程发送 SIGKILL-9会导致整个进程终止因为进程内的所有线程共享同一个地址空间无法单独“杀死”一个线程而不影响进程整体的完整性除非该线程捕获并处理了非致命信号。3. 等待线程pthread_join函数解释新线程在被主线程创建后也需要等待否则也可能会出现类似于僵尸进程造成泄漏的问题有时也需要知道子线程退出结果。线程等待的接口为pthread_join函数原型如下#include pthread.h int pthread_join(pthread_t thread, void **retval);参数说明thread 这是你要等待的那个线程的 ID即创建线程时 pthread_create 第一个参数传出的那个 ID。retval 这是一个二级指针指向指针的指针。1如果你不关心子线程的返回值可以将其设为 NULL。2如果你关心子线程的退出状态可以传入一个 void * 类型的变量地址。函数返回后这个变量里就会存着子线程 return 或者 pthread_exit 这个函数用于线程退出详情在后面传出来的值。返回值成功则返回 0。失败则返回错误码注意pthread_join 会阻塞式的等待。这意味着调用它的主线程会“卡”在这里直到目标线程彻底执行完毕才能继续执行。如果目标线程死循环了主线程也会永远卡住。示例让主线程创建子线程后直接调用pthread_join等待而子线程运行5秒后才退出代码实现如下#include iostream #include pthread.h #include unistd.h using namespace std; void *threadRun(void* args) { int cnt 0; while(1) { cout new thread is running ... endl; sleep(1); cnt; if(cnt 5) break; } return nullptr; } int main() { pthread_t tid; pthread_create(tid, nullptr, threadRun, nullptr); pthread_join(tid, nullptr); // 阻塞等待tid对应的线程不关心线程退出情况 cout main thread quit! endl; // 因为阻塞等待所以这段代码会最后执行 return 0; }运行结果第二个参数 retval 怎么使用pthread_join 的第二个参数 retval 用于接收子线程通过 return 或 pthread_exit() 传出的数据地址。子线程运行时可以通过 return value; 或者调用 pthread_exit(value); 来表示线程终止退出了此时会交出一个退出码 value这个 value 是一个指针类型void *的因为线程回调函数的返回值就是void*所以要获取这个指针我们首先需要在外部定义一个void* 的变量假如定义为 void *ret然后对这个变量来作为第二个参数传递过去即传递 ret之后在pthread_join执行过程中就会将线程中的退出结果 value 直接赋值给 *retval *retval 就相当于 ret 变量即 *retval value 这就实现了我们定义的变量 ret value 的过程。示例比如线程退出时返回 100 则我们就可以通过上面的方法得到一个已经赋值的 void * 变量此时我们只需要对其做强转就可以得到线程退出时的退出码了。代码如下#include iostream #include pthread.h #include unistd.h using namespace std; void *threadRun(void* args) { int cnt 0; while(1) { cout new thread is running ... endl; sleep(1); cnt; if(cnt 5) break; } // 模拟计算 int *result (int *)malloc(sizeof(int)); *result 100; // 不能返回局部变量的地址 return (void *)result; } int main() { pthread_t tid; pthread_create(tid, nullptr, threadRun, nullptr); void *ret; // 用来存放接收到的返回值 pthread_join(tid, ret); // 要关心线程退出情况 int *res_ptr (int *)ret; // 需要做类型转换 cout 主线程: 收到子线程的返回值: *res_ptr endl; free(res_ptr); // 注意释放 cout main thread quit! endl; return 0; }运行结果4. 线程退出pthread_exit让线程退出的最简单方法就是通过 return 语句实现。除此之外还可以在线程执行代码中调用pthread_exit函数实现。注意使用 exit 是用来终止进程的不能用来终止线程。一旦在某个线程代码执行了exit则会将整个进程终止。函数说明#include pthread.h void pthread_exit(void *retval);retval 这是一个 void * 类型的指 它代表的是线程执行函数的返回值。示例#include iostream #include pthread.h #include unistd.h using namespace std; void *threadRun(void* args) { int cnt 0; while(1) { cout new thread is running ... endl; sleep(1); cnt; if(cnt 5) break; } pthread_exit((void *)100); // 线程退出将100转成地址作为返回值 } int main() { pthread_t tid; pthread_create(tid, nullptr, threadRun, nullptr); void *ret; // 用来存放接收到的返回值 pthread_join(tid, ret); // 关心线程退出情况 cout 主线程: 收到子线程的返回值: (long)ret endl; // 此时ret就是100转成的地址转成long是因为void*大小和long一样 cout main thread quit! endl; return 0; }5. 线程取消pthread_cancel与 pthread_exit线程主动退出不同pthread_cancel 是被动的。它允许一个线程向另一个已经启动的线程发送“取消请求”请求对方终止运行。函数如下#include pthread.h int pthread_cancel(pthread_t thread);参数目标线程的 ID即创建线程时 pthread_create 第一个参数传出的那个 ID。返回值成功返回 0失败返回错误码。 函数返回成功仅代表取消请求发送成功并不代表目标线程已经立即终止。如果线程被别的线程调用pthread_ cancel异常终掉pthread_join 第二个参数所指向的单元里存放的是常数 PTHREAD_ CANCELED一个宏其实就是 -1 。三、线程的 tid 的深入理解我们知道使用pthread_create创建线程时会通过第一个参数得到线程在用户层的唯一标识符tid但是我们也可以通过指令ps -aL看到线程在内核层的唯一标识符LWP。它们其实是不一样的可以看一下代码#include iostream #include string #include pthread.h #include unistd.h using namespace std; string toHex(pthread_t tid) { char hex[64]; snprintf(hex, sizeof(hex), %p, tid); return hex; } void *threadRoutine(void *args) { while(true) { cout thread id: toHex(pthread_self()) endl; sleep(1); } } int main() { pthread_t tid; pthread_create(tid, nullptr, threadRoutine, (void*)thread 1); cout main thread create thead done, new thread id : toHex(tid) endl; pthread_join(tid, nullptr); return 0; }注其中 pthread_ self 函数是由线程库NPTL提供的可以获得线程自身的 tid 值。运行结果如下那么为什么这两个值不一样呢请看以下分析。操作系统内核只会管理执行流(轻量级线程)因为Linux操作系统内核中没有线程的概念只有轻量级进程的概念所以在操作系统内核中才只有一个的 task_struct 对应的执行流如图所以操作系统就没有提供线程的系统调用只个提供轻量级进程的系统调用。在Linux中在底层专门创建轻量级进程的系统调用叫做 clone 如下看到这个函数中的参数很多但我们需要关注其中两个第一个参数是一个函数指针表示新执行流要执行的代码第二个参数表示的是我们创建一个新线程时自定义的一个栈其他参数暂时不管。这个接口我们用不了所以它就被封装了被 pthread 线程库封装了所以就有了我们使用的函数接口比如 pthread_createpthread_join......。线程库在用户层管理线程线程库如 glibc 中的 libpthread本质上也是一个动态链接库需要加载于到内存同时映射到进程地址空间的共享区。当我们在代码中调用 pthread_create 时线程库会在用户态内存中分配并初始化线程控制块和用户栈。随后线程库通过系统调用 clone 将新线程的入口函数地址以及新分配的独立栈地址传递给内核之后内核才据此创建一个新的执行流轻量级进程即创建并维护新的task_struct。因此线程的概念是由线程库在用户层维护的。所以线程库要维护线程的概念不用维护线程的执行流这就注定了线程库要维护多个线程的概念所以线程库就要管理这些线程由先描述再组织可知线程库中一定有描述线程的线程控制块TCB。所以我们每创建一个线程就要在线程库中创建线程的线程控制块这个线程控制块中就包含了这个线程的独立栈在哪里回调函数在哪里指向内核中的哪一个执行流......等等属性。所以整个结构如图所示很明显这个线程是在用户层维护的所以我们又称其为用户级线程。线程的 tid线程的TCB是由线程库维护的线程库是映射在进程地址空间的共享区的。但其实当你调用 pthread_create 时线程库会在进程的共享内存区域通常是通过 mmap 分配的匿名映射区位于堆和栈之间的高地址区域分配一块内存这个内存块中包含了struct pthread (TCB)程局部存储线程栈三部分但它们一般都是在一起的。当维护了很多个线程时在操作系统中的就会得到如下这个图的结构而其中我们就把线程内存块在内存中的起始地址称为 tid也是TCB的起始地址。也就是pthread_create的第一个参数指向的值。注意因为每一个线程在运行时都有自己的调用链所以每一个线程都有自己独立的栈结构。除了主线程其他所有线程的独立栈都在共享区具体来说pthread库中的tid指向的内存块中。而主线程的栈就是共享区上面的那个栈如上图。四、多线程的使用多线程的创建创建一个线程我们可以使用一个pthread_create和一个pthread_join实现。如果要创建多个线程则我们可以使用多个pthread_create创建同时配合多个pthread_join实现。示例代码#include iostream #include vector #include string #include pthread.h #include unistd.h using namespace std; #define NUM 5 // 这个结构体只是为了方便观察线程 struct threadData { string threadname; }; // 按照16进制打印 string toHex(pthread_t tid) { char buffer[128]; snprintf(buffer, sizeof(buffer), 0x%x, tid); return buffer; } void *threadRoutine(void *args) { threadData *td static_castthreadData*(args); // 类型转换 while(true) { cout threadname: td-threadname , thread id: toHex(pthread_self()) endl; sleep(1); } } void InitThreadData(threadData *td, pthread_t number) { td-threadname thread- to_string(number); } int main() { vectorpthread_t tids; // 保持每个线程的tid // 创建多线程 for(int i 1; i NUM; i) { pthread_t tid; threadData *td new threadData; InitThreadData(td, i); // 初始化一下线程名称 pthread_create(tid, nullptr, threadRoutine, td); tids.push_back(tid); } // 等待多线程 for(int i 0; i NUM; i) { pthread_join(tids[i], nullptr); } return 0; }在上面这个代码中所有的线程都会执行 threadRoutine 这同一个函数。线程的栈如果我们在上面的这个threadRoutine中定义一个变量并打印其地址则会发现每一个在线程执行函数中定义的变量的地址都是不同的。示例代码只有threadRoutine 部分其余部分和上面一样void *threadRoutine(void *args) { threadData *td static_castthreadData*(args); // 类型转换 int val 10; // 局部变量 while(true) { cout threadname: td-threadname , val: val , val: val endl; sleep(1); } }通过运行结果库看到这里每个线程打印出的val变量的地址都是不同的。所以每个线程都有自己独立的栈结构。虽然每个线程的栈是独立的但是线程之间是几乎没有秘密的线程栈上的数据也可以被其他线程看到并访问只要一个线程持有另一个线程栈上变量的指针它就可以直接读取甚至修改该数据。不过这种跨栈访问通常是不安全的所以不建议因为一旦目标线程执行结束其栈内存会被回收导致指针失效。线程的局部存储全局变量可以被所有线程直接看到所以它是共享资源。正因为共享资源可以被其他线程看到所以其他线程就可以随时修改它这就会导致因为数据更新的数据不一致问题关于这个问题我们在下一篇文章再谈因为涉及同步与互斥的内容。线程如果想要一个私有的全局变量即一个不被所有线程直接看到全局变量则可以在全局变量定的前面加上__thread。代码如果直接定义全全局变量则它就是别所有线程看到的任何一个先对其操作都是作用在同一个变量上的代码如下#include iostream #include vector #include string #include pthread.h #include unistd.h using namespace std; #define NUM 3 int g_val 100; // 共享的全局变量 // 这个结构体只是为了方便观察线程 struct threadData { string threadname; }; // 按照16进制打印 string toHex(pthread_t tid) { char buffer[128]; snprintf(buffer, sizeof(buffer), 0x%x, tid); return buffer; } // void *threadRoutine(void *args) // { // threadData *td static_castthreadData*(args); // 类型转换 // int val 10; // 局部变量 // while(true) // { // // cout threadname: td-threadname , thread id: toHex(pthread_self()) endl; // cout threadname: td-threadname , val: val , val: val endl; // sleep(1); // } // } void *threadRoutine(void *args) { threadData *td static_castthreadData*(args); // 类型转换 while(true) { printf(threadname: %s, g_val: %d, g_val: %p\n, td-threadname.c_str(), g_val, g_val); g_val; // 所有线程都会对同一个变量 sleep(1); } } void InitThreadData(threadData *td, pthread_t number) { td-threadname thread- to_string(number); } int main() { vectorpthread_t tids; // 保持每个线程的tid // 创建多线程 for(int i 1; i NUM; i) { pthread_t tid; threadData *td new threadData; InitThreadData(td, i); // 初始化一下线程名称 pthread_create(tid, nullptr, threadRoutine, td); tids.push_back(tid); } // 等待多线程 for(int i 0; i NUM; i) { pthread_join(tids[i], nullptr); } return 0; }运行结果加上__thread之后得到的运行结果如下注意这个__thread是编译器提供的不是c/c通过的它只能定义内置类型不能修饰自定义类型会报错。上面这种每个线程都访问同一个全局变量但是它们在访问的时候都是各自私有一份的这种技术我们称为线程的局部存储。所以之前线程库在共享区管理线程时看到一个子线程在内存中实际上是由三个连续的部分组成的这三个部分为struct pthread (TCB)线程控制块。线程局部存储 (TLS)存放 __thread 变量的地方。线程栈 (Thread Stack)存放局部变量的地方。即五、线程分离正常情况下主线程创建了新线程后是要使用 pthread_join 进行等待但这种等待时阻塞式的如果主线程要做其他事情这样就会卡住而如果主线程不等待则就无法释放资源从而会造成系统泄漏。那么这里就有一种方法线程分离。如果不关心线程的返回值pthread_join 就是一种负担这个时候我们可以告诉系统当线程退出时让它自动释放线程资源。这种方法就是线程分离让线程处于分离状态。使用的函数为该函数就是用于将指定的线程设置为“分离状态”其中的参数 thread 就是要指定的线程 tid。调用该函数有两种常见的时机由其他线程调用 主线程在创建完子线程后如果确定不需要等待子线程结束可以立即调用pthread_detach(tid)。由线程自己调用 子线程在运行函数内部调用pthread_detach(pthread_self())即可实现“自分离”。这通常用于那些一旦启动就不需要与创建者交互的后台任务线程。注意如果对一个已经分离的线程调用 pthread_join函数会失败。所以pthread_join和pthread_detach是冲突的。代码示例主线程来分离#include iostream #include vector #include string #include pthread.h #include unistd.h #include string.h using namespace std; #define NUM 3 __thread int g_val 100; // 共享的全局变量 // 这个结构体只是为了方便观察线程 struct threadData { string threadname; }; // 按照16进制打印 string toHex(pthread_t tid) { char buffer[128]; snprintf(buffer, sizeof(buffer), 0x%x, tid); return buffer; } void *threadRoutine(void *args) { threadData *td static_castthreadData*(args); // 类型转换 while(true) { cout threadname: td-threadname , thread id: toHex(pthread_self()) endl; sleep(1); } delete td; // 注意释放内存 return nullptr; } void InitThreadData(threadData *td, pthread_t number) { td-threadname thread- to_string(number); } int main() { vectorpthread_t tids; // 保持每个线程的tid // 创建多线程 for(int i 1; i NUM; i) { pthread_t tid; threadData *td new threadData; InitThreadData(td, i); // 初始化一下线程名称 pthread_create(tid, nullptr, threadRoutine, td); tids.push_back(tid); } sleep(2); for(auto tid : tids) { pthread_detach(tid); // 让所有新线程都分离 } // 等待多线程 for(int i 0; i NUM; i) { int ret pthread_join(tids[i], nullptr); // 分离后这个会调用失败 if(ret ! 0) { printf(ret %d, who 0x%x, why: %s\n, ret, tids[i], strerror(ret)); } else printf(join success\n); } return 0; }由线程自己分离#include iostream #include vector #include string #include pthread.h #include unistd.h #include string.h using namespace std; #define NUM 3 __thread int g_val 100; // 共享的全局变量 // 这个结构体只是为了方便观察线程 struct threadData { string threadname; }; // 按照16进制打印 string toHex(pthread_t tid) { char buffer[128]; snprintf(buffer, sizeof(buffer), 0x%x, tid); return buffer; } void *threadRoutine(void *args) { pthread_detach(pthread_self()); // 由线程自己分离 threadData *td static_castthreadData*(args); // 类型转换 int cnt 0; while(true) { cout threadname: td-threadname , thread id: toHex(pthread_self()) endl; cnt; if(cnt 3) break; sleep(1); } delete td; // 注意释放内存 return nullptr; } void InitThreadData(threadData *td, pthread_t number) { td-threadname thread- to_string(number); } int main() { vectorpthread_t tids; // 保持每个线程的tid // 创建多线程 for(int i 1; i NUM; i) { pthread_t tid; threadData *td new threadData; InitThreadData(td, i); // 初始化一下线程名称 pthread_create(tid, nullptr, threadRoutine, td); tids.push_back(tid); } sleep(5); // 保证主线程是最后退出的 // 等待多线程 for(int i 0; i NUM; i) { int ret pthread_join(tids[i], nullptr); // 分离后这个会调用失败 } return 0; }注意即使线程分离了但是仍然要保证主线程是最后退出的。因为如果主线程退了则正在运行的新线程也会被立即杀掉这样已经分离的线程就没有机会完成它的任务可能会导致程序逻辑中断或数据丢失。感谢各位观看希望大家多多支持
返回列表