【Linux】线程概念与控制(3):线程ID&&C++封装线程

目录

[一 线程ID及进程地址空间分布](#一 线程ID及进程地址空间分布)

[二 线程局部存储](#二 线程局部存储)

[三 C++封装pthread类](#三 C++封装pthread类)


一 线程ID及进程地址空间分布

线程ID不是WLP,是地址,是虚拟地址;这个虚拟地址一定属于进程虚拟地址空间上的地址

pthread库:你的程序要加载到内存,变成进程,形成地址空间,那么它所依赖的动态库要不要加载到内存?

要!

理解用户线程:

在内核和用户之间,增加一层pthread库,在这个库的内部,有没有新建的,运行的线程?

线程会呈现出不同的状态,库内必须要对线程进行管理--->先描述,再组织(这里的管理不是操作系统的任务)

所以在pthread库里面,存在描述线程的结构体:struct thread{ .....};这个结构体就是传说中的TCB

创建线程,就会在库里面创建描述线程的结构体 struct pthread(叫做用户级线程),同时在内核中创建PCB,属于LWP,这两个合起来叫做Linux下的线程解决方案

外面的人并不知道LWP在里面帮忙跑

struct pthread一定在库里面,把描述这个结构体的起始地址,叫做线程ID

每创建一个线程,就是在库里面创建一小块空间,返回起始地址

pthread动态库内部包含三部分信息:struct pthread,线程局部存储,线程栈;这三部分信息合起来就是一个线程相关的信息

把整个给线程描述的内存块,叫做当前线程的tid

每个线程都有自己私有的东西,上下文信息和线程栈

每创建一个新线程,在动态库内部,都会维持一个结构,所以新线程的栈结构是在库内部维护的


二 线程局部存储

每个线程有一个独有的全局变量:TLS

它看起来像全局变量,所有函数都能访问,实际上每个线程有自己独立的副本;线程之间完全隔离,互不干扰,不需要加锁,天生线程安全

为什么需要 TLS?

普通全局变量是所有线程共享的:

复制代码
// 全局变量:所有线程共享
int g_count;

多线程同时修改会出现数据竞争、结果错乱,必须加互斥锁,效率低、代码复杂。

TLS 解决:让每个线程有自己的 "私有全局变量"

TLS的使用方法:
C11 / GCC 直接用关键字声明,一行代码搞定:

cpp 复制代码
// 声明 TLS 变量:每个线程独立一份
__thread int tls_num;//注意这里是两个下划线
// C11 标准写法
_Thread_local int tls_val;

三 C++封装pthread类

我们先来给全部的代码然后解释:

Thread.hpp

cpp 复制代码
#ifndef __THREAD_HPP
 #define __THREAD_HPP
 
 #include <iostream>
 #include <string>
 #include <functional>
 #include <pthread.h>
 #include <unistd.h>
 #include <sys/syscall.h> /* Definition of SYS_* constants */
 
 using func_t = std::function<void()>;
 
 enum class TSTATUS//线程状态
 {
     THREAD_NEW,
     THREAD_RUNNING,
     THREAD_STOP
 };
 
 // bug
 static int gnum = 1;
 
 class Thread
 {
 private:
     void getprocessid()
     {
         _pid = getpid();
     }
     void getlwp()
     {
         _lwpid = syscall(SYS_gettid);
     }
     static void *routine(void *args)
     {
         Thread *ts = static_cast<Thread *>(args);
         ts->getprocessid();
         ts->getlwp();
         pthread_setname_np(pthread_self(), ts->Name().c_str());
         ts->_func();
 
         return nullptr;
     }
 
 public:
     Thread(func_t f)
        : _joinable(true),
          _status(TSTATUS::THREAD_NEW),
          _func(f)
     {
         _name = "thread-" + std::to_string(gnum++);
     }
     void start()
     {
         if (_status == TSTATUS::THREAD_RUNNING)
         {
             std::cout << "thread is already running" << std::endl;
             return;
         }
         int n = pthread_create(&_tid, nullptr, routine, this);
         (void)n;
         _status = TSTATUS::THREAD_RUNNING;
     }
     void stop()
     {
         if (_status == TSTATUS::THREAD_RUNNING)
         {
             int n = pthread_cancel(_tid);//只有运行中才能终止
             (void)n;
             _status = TSTATUS::THREAD_STOP;
         }
         else
         {
             std::cout << "thread status is : THREAD_NEW or THREAD_STOP! stop error" << std::endl;
         }
     }
     void join()
     {
         if (_joinable)
         {
             int n = pthread_join(_tid, nullptr);
             (void)n;
             printf("lwp : %d, name: %s, join success\n", _lwpid, _name.c_str());
         }
         else{
             printf("lwp : %d, name: %s, join failed, because thread is detach\n", _lwpid, _name.c_str());
         }
     }
     void detach()
     {
         if (_joinable && _status == TSTATUS::THREAD_RUNNING)
         {
             _joinable = false;
             int n = pthread_detach(_tid);
             (void)n;
         }
     }
     std::string Name()
     {
         return _name;
     }
     ~Thread()
     {
     }
 
 private:
     pthread_t _tid;
     pid_t _pid;
     pid_t _lwpid;
     std::string _name;
     bool _joinable;
     TSTATUS _status;
     func_t _func;
 };
 
 #endif

main.cc

cpp 复制代码
#include <iostream>
 #include <unistd.h>
 #include <vector>
 #include "Thread.hpp"
 
 void hello()
 {
     char name[64];
     pthread_getname_np(pthread_self(), name, sizeof(name));
 
     int cnt = 5;
     while(cnt--)
     {
         std::cout << "hello thread: " << name << std::endl;
         sleep(1);
     }
 }
 
 int main()
 {
     std::vector<Thread> threads;
     for(int i = 0; i < 10; i++)
     {
         threads.emplace_back(hello);
     }
 
     for(auto &thread : threads)
         thread.start();
 
     // for(auto &thread : threads)
     //     thread.detach();
 
     // for(auto &thread : threads)
     //     thread.start();
 
     for(auto &thread : threads)
         thread.join();
 
 
     sleep(3);
 
     // Thread t(hello);
     // t.start();
 
     // sleep(5);
 
     // t.stop();
 
     // t.join();
     // sleep(3);
     return 0;
 }

(1)ts是什么?
ts就是this指针!!

你创建线程时,传了 this

cpp 复制代码
int n = pthread_create(&_tid, nullptr, routine, this);

这个参数就表示会把this当作参数传给入口函数routine

最后一个参数 this = 当前这个 Thread 对象的地址

线程启动后,this 被传给了 args

cpp 复制代码
static void *routine(void *args)

此时:

args 里面存的就是 主线程传进来的 this

但它是 void*,编译器不认识它是 Thread*

于是你做了类型转换

cpp 复制代码
Thread *ts = static_cast<Thread *>(args);

(2)什么是static_cast?

static_cast = C++ 里最安全、最常用的 "类型转换" 工具
作用:把一种类型,强制转换成你想要的另一种类型

cpp 复制代码
Thread *ts = static_cast<Thread *>(args);

意思就是:

把 args 这个 void 类型,转换成 Thread 类型

(3)为什么routine要设置成static类型?

pthread_create 是 C 语言函数,不认识 C++ 类成员函数(非静态成员函数),但是静态成员函数是没有this指针的,就不会和后面调用routine参数冲突

(4)什么是to_string?

cpp 复制代码
_name = "thread-" + std::to_string(gnum++);

std::to_string 就是 C++ 里的一个工具函数,作用只有一个:
把数字 → 转成字符串

gnum 是 int 数字:1、2、3......;std::to_string(gnum) 把它变成 "1"、"2、"3"`;然后才能和 "thread-" 拼接成线程名:"thread-1"、"thread-2"

(5)为什么pthread类里面有两个private?为什么在构造函数里面为什么只初始化了一部分?

只是把函数和变量分开写,对代码语法和功能没有任何影响

构造函数只初始化了 3 个,但类里有 6 个 成员变量!

答案:剩下的 4 个,会在【线程运行时】自动赋值

cpp 复制代码
_tid;       →  pthread_create 后自动赋值
_pid;       →  新线程里 getpid()
_lwpid;     →  新线程里 syscall(SYS_gettid)
_name;      →  构造函数里自己赋值

(6)创建好线程对象不代表线程已经启动了;当它start时,线程才会被创建

在代码中我们要设计线程的状态:运行,新建,不运行...... --->用枚举

cpp 复制代码
 enum class TSTATUS//线程状态
 {
     THREAD_NEW,
     THREAD_RUNNING,
     THREAD_STOP
 };
 

并且在private中增加TSTATUS_status;

让一个线程进行start:首先一个线程是不能重复启动的,只有线程处于new或stop,才能启动;所以线程处于running状态不能启动

让一个线程进行stop:线程必须处于running状态才能stop

让一个线程进行detach:线程是_joinable和running的

未来可以把线程模板化

(7)怎么知道真正运行的是哪一个线程?

利用局部存储把线程名字传递给指定参数

cpp 复制代码
std::string name()
{
  return name;
}

库函数:pthread_setname_np: 调用该函数的线程,可以根据指定线程名给线程设定名称;一旦设定,就在pthread库里面记录线程名和ID的关系。之后就可以使用pthread_setname_np,得到指定ID线程的名字

在static void routine()中增加pthread_getname_np,在void hello()中增加pthread_getname_np;所以传递线程名可以跨函数

相关推荐
消失的旧时光-19431 小时前
C 语言如何实现“面向对象”?—— 从 struct + 函数指针,到 Linux 内核设计思想
linux·c语言·开发语言
handler011 小时前
滑动窗口(同向双指针)算法:模板与例题解析
c语言·c++·笔记·算法·蓝桥杯·双指针·滑动窗口
不做无法实现的梦~1 小时前
Linux 新手到日常运维操作指南
linux·运维·服务器
xingfujie1 小时前
第3章 安装 kubeadm/kubelet/kubectl
linux·云原生·容器·kubernetes·kubelet
Brilliantwxx1 小时前
【算法题】基础计算器的不同实现方式
c++·算法
Sunsets_Red1 小时前
P12375 「LAOI-12」MST? 题解
c++·算法·洛谷·信息学·oier·洛谷题解
测试员周周1 小时前
【Appium 系列】第09节-数据驱动测试 — YAML 数据 + parametrize
服务器·数据库·人工智能·python·测试工具·语言模型·appium
不能隔夜的咖喱1 小时前
黑马ai大模型笔记(自用,比较粗糙)
linux·windows·python
暴力求解1 小时前
Linux--网络-->UDP_socket
linux·网络·网络协议·udp·操作系统