目录
1.线程的tid(本质是线程属性集合的起始虚拟地址)
1.1pthread库中线程的tid是什么?
1.2理解库
1.3phtread库中做了什么?
1.4线程的tid,和内核中的lwp
1.5线程的局部存储
2.封装管理原生线程库
1.线程的tid(本质是线程属性集合的起始虚拟地址)
1.1pthread库中线程的tid是什么?
首先我们写一个程序获取线程id:
我们可以看到给用户提供的线程ID,不是内核中的LWP,而是pthread库中维护的一个唯一值,库内部也要承担对线程的管理。
#include <iostream> #include <string> #include <vector> #include <thread> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void*threadrun(void *args) { std::string name =static_cast<const char*>(args); while(true) { std::cout<<name<<"is running,tid: "<<pthread_self()<<std::endl; sleep(1); } } int main() { pthread_t tid; pthread_create(&tid,nullptr,threadrun,(void*)"thread-1"); std::cout<<"new thread tid: "<<tid<<std::endl; pthread_join(tid,nullptr); return 0; }
为了方便查看,我们将tid转成16进制:
#include <iostream> #include <string> #include <vector> #include <thread> #include <stdlib.h> #include <pthread.h> #include <unistd.h> std::string ToHex(pthread_t tid) { char id[128]; snprintf(id, sizeof(id), "0x%lx", tid); return id; } void*threadrun(void *args) { std::string name =static_cast<const char*>(args); while(true) { std::string id = ToHex(pthread_self()); std::cout<<name<<"is running,tid: "<<id<<std::endl; sleep(1); } } int main() { pthread_t tid; pthread_create(&tid,nullptr,threadrun,(void*)"thread-1"); std::cout<<"new thread tid: "<<ToHex(tid)<<std::endl; pthread_join(tid,nullptr); return 0; }
转为16进制我们可以看出来tid实际上是一个地址
1.2理解库
首先我们要知道pthread库实际上是Linux中的一个文件,这个文件我们称它为:pthread库。这个库默认我们没有运行多线程时,他是在磁盘上的,他是一个动态库
我们生成的可执行程序在没运行的时候,当然也是在磁盘中的。当运行的时候,我们的可执行程序要加载到内存中,程序要变成一个进程的时候,它的PCB和内核数据结构要被创建出来,通过页表映射到可执行程序的代码和数据
多线程在启动之前首先要是一个进程,然后调用接口动态的创建多线程。调用接口创建线程,前提是把库加载到内存,映射到进程的地址空间!!!若正文部分调用了创建线程的接口,会跳转到共享区中的库中,库再通过页表映射在内存中,找到实现方法,在库中就把线程创建好了。
1.3phtread库中做了什么?
我们先不管OS,库是如何做到对线程进行管理呢?(先描述,再组织)
库中创建一个线程,会为线程申请一个内存块(每创建一个线程申请一个内存块),所有的块都是连续存储的,内存块就是一个大号的结构体,内存块中存在线程在用户及最基本的属性和线程栈,这个栈是线程独立的栈结构。未来我们想要找一个线程的属性直接找到线程管理控制块的地址即可,这个地址就是tid!
举个例子,join是怎么拿到退出结果的?
在线程执行流结束的时候,库中给线程的内存块是没有被释放的,新线程会把退出结果在线程属性中用(void*)的变量维护起来,只有join了它才会释放。join函数通过线程的地址找到线程的内存块,再将新线程的退出结果拷贝回来。
每个栈是如何独立的
每个线程都有自己独立的线程控制块。每个线程在创建时都会分配一个独立的栈空间(在自己的控制块中开辟一段合适的内存空间就行了,这个空间大小是可以动态调整的),用于存储线程执行过程中的局部变量、函数调用等信息。这个栈空间是线程私有的,其他线程无法直接访问。新线程的栈在自己的控制块内,主线程的栈是地址空间中的栈。
1.4线程的tid,和内核中的lwp
首先我们要知道在Linux内核中,线程通常是通过轻量级进程(LWP)来实现的。LWP是内核级别的线程,由操作系统内核直接管理和调度。它们共享同一进程的资源(如内存空间、文件描述符等),但每个LWP都有自己独立的执行上下文和调度状态。他是有自己的系统调用的,比如创建轻量级进程的系统调用:clone,它可以让LWP去执行clone设置的回调函数形成临时变量放在,放在你所指明的栈空间里。所以libpthread.so库就是封装了创建轻量级进程的系统调用:clone
在用户层我们有libpthread.so库,当用户在用户态通过pthread库等线程库创建线程时会有对应的PCB,最终会被1:1被映射到内核级的LWP上,LWP的表现实际上就是PCB,用来调度和管理轻量级进程。所以线程的概念,只是在库中表现出来的,所以我们把Linux中的线程称之为:用户级线程。所以Linux 线程=pthread库中线程的属性级+LWP,这就是1:1级别用户级线程库的实现
1.5线程的局部存储
示例:新线程(对gval做++)和主线程都在打印全局变量gval的值和地址,一旦全局变量被修改两者是都能看见的,因为都在一个进程内,共享一个地址空间。这种全局变量本身就是多线程之间共享的。
#include <iostream> #include <string> #include <vector> #include <thread> #include <stdlib.h> #include <pthread.h> #include <unistd.h> int gval=100; std::string ToHex(pthread_t tid) { char id[128]; snprintf(id, sizeof(id), "0x%lx", tid); return id; } void*threadrun(void *args) { std::string name =static_cast<const char*>(args); while(true) { std::string id = ToHex(pthread_self()); std::cout << name << " is running, tid: " << id << ", gval: " << gval << ", &gval: " << &gval << std::endl; gval++; sleep(1); } } int main() { pthread_t tid; pthread_create(&tid,nullptr,threadrun,(void*)"thread-1"); while(true) { std::cout << "main thread, gval: " << gval << ", &gval: " << &gval << std::endl; sleep(1); } pthread_join(tid,nullptr); return 0; }
但如果我想让gval在新线程和主线程中各自私有一份,我们在这里就要使用:__thread
__thread int gval=100;
看效果:此时新线程中和主线程中的gval值不一样地址也不一样,显然它们用的gval不是同一个了。
当使用了__thread关键字后,GCC会在每个线程的上下文中为该变量创建一个独立的实例。这样,每个线程都可以独立地修改其对应的变量实例,而不会影响到其他线程。并且,被__thread修饰的变量会被存储在各自线程的局部存储中,这种存储方式确保了线程间的数据隔离。__thread只在Linux下有效,而且只能修饰内置类型
2.封装管理原生线程库
C++11中的线程创建,其实就是对原生线程的封装,现在我们也来封装一下原生线程:
Thread.hpp(注释就是实现思路)
#pragma once #include <iostream> #include <string> #include <pthread.h> namespace ThreadMoudle { // 线程要执行的方法,后面我们随时调整 typedef void (*func_t)(const std::string &name); // 函数指针类型 //执行函数时,名字带出来,方便打印测试结果 class Thread { public: //成员方法调用_func执行任务 void Excute() { std::cout << _name << " is running" << std::endl; _isrunning = true;//开始回调了,就表示线程跑起来了 _func(_name); _isrunning = false; } public: //构造 Thread(const std::string &name, func_t func):_name(name), _func(func) { std::cout << "create " << name << " done" << std::endl; } // 线程的固定历程,新线程都会执行该方法! static void *ThreadRoutine(void *args) { //为了匹配类型,加static属于类而不属于对象,就没有this指针了 //this指针从creat函数传递过来 Thread *self = static_cast<Thread*>(args); // 获得了当前对象 self->Excute();//直接调用成员方法 return nullptr;//简单的演示,没有设置返回值 } //线程启动 bool Start() { //使用标准库中的方法创建进程 int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this); if(n != 0) return false; return true; } //表示状态 std::string Status() { if(_isrunning) return "running"; else return "sleep"; } void Stop() { //表示有线程在running才需要stop if(_isrunning) { ::pthread_cancel(_tid);//取消 _isrunning = false;//状态变为停止 std::cout << _name << " Stop" << std::endl; } } void Join() { //线程退出后等待回收。 if(!_isrunning) { ::pthread_join(_tid, nullptr); std::cout << _name << " Joined" << std::endl; } } //知道是哪个线程 std::string Name() { return _name; } ~Thread() { } private: std::string _name;//线程名字 pthread_t _tid;//ID bool _isrunning;//是否在运行 func_t _func; // 线程要执行的回调函数(任务) }; }
Main.cc
#include <iostream> #include <vector> #include <cstdio> #include <unistd.h> #include "Thread.hpp" using namespace ThreadMoudle; void Print(const std::string &name) { int cnt = 1; while (true) { std::cout << name << "is running, cnt: " << cnt++ << std::endl; sleep(1); } } const int gnum = 10; int main() { // 我在管理原生线程, 先描述,在组织 // 构建线程对象 std::vector<Thread> threads; for (int i = 0; i < gnum; i++) { std::string name = "thread-" + std::to_string(i + 1); threads.emplace_back(name, Print); sleep(1); } // 统一启动 for (auto &thread : threads) { thread.Start(); } sleep(10); // 统一结束 for (auto &thread : threads) { thread.Stop(); } // 等待线程等待 for (auto &thread : threads) { thread.Join(); } return 0; }
运行结果: