目录
[一 线程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
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;所以传递线程名可以跨函数