📝前言:
这篇文章我们来讲讲Linux——线程控制
🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏
目录
一,使用介绍
1. POSIX线程库
- 对于Linux内核而言,没有线程,只有轻量级进程。但是对于用户而言,需要有线程。
- 所以,
pthread
(用户态库)就对Linux的轻量级进程进行了封装,为用户提供线程接口(而把Linux内部的轻量级进程隐藏起来) - 头文件:
<pthread.h>
- 编译时要连接对应的库:
-lpthread
(但是:现合并到libc
,不用显式链接也能跑)
某些语言自己的线程库,本质上都是对OS的线程库操作的封装。如,C++的线程库,他会封装Linux的线程库pthread
,也会封装Windows的线程库,然后再根据自己的运行环境,通过条件编译选择对应的实现版本。
2. 创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- 功能:创建一个新线程,让新线程执行对应的函数
- 参数
thread
:返回线程ID(这是属于语言层的线程ID,内核并不认识)attr
:设置线程的属性,为NULL
表示使用默认属性start_routine
:函数地址,线程启动后要执行的函数arg
:传给线程启动函数的参数
- 返回值:
- 成功返回
0
- 失败返回错误码(
pthreads
函数出错时不会设置全局变量errno
)
- 成功返回
3. 结束线程
结束进程有三种方法:
return
:最推荐(不能return
局部变量)pthread_exit(void *value_ptr)
:终止自己,不会影响其他线程(等效于return
)value_ptr
返回值,不能是局部变量(这和后续获取返回值有关,下文会讲述如何获取返回值)
pthread_ cancel(pthread_t thread)
:取消其他线程(注意,只能取消已经启动的线程)- 注意:不能使用
exit()
:该函数为整个进程退出
4. 等待线程
和进程一样,主线程需要等待线程(其实目的也一样,后续会讲到对应的结构更好理解)
int pthread_join(pthread_t thread, void **value_ptr);
- 功能:等待线程(该等待只有阻塞等待)
- 参数
thread
:线程IDvalue_ptr
:它指向⼀个指针,后者指向线程的返回值
- 返回值:
- 成功返回
0
- 失败返回错误码
- 成功返回
5. 分离线程
默认情况下,新创建的线程是joinable
的(即:需要等待),如果我们不关心线程的退出信息,可以进行分离线程,让线程退出时,自己释放资源。
int pthread_detach(pthread_t thread)
- 功能,分离线程(可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离)
- 参数
thread
:线程ID
- 分离以后再
join
就会出错
二,使用示例
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *thread(void *num) // 要求返回值和参数类型都是 void*
{
cout << "我是线程 " << *((int *)num) << " 我的ID是: " << pthread_self() << endl;
sleep(5);
cout << "线程 " << *((int *)num) << " 退出" << endl;
return num;
}
int main()
{
pthread_t threads_id[5];
for (int i = 1; i <= 5; i++)
{
pthread_t thread_id;
pthread_create(&threads_id[i - 1], nullptr, thread, (void *)&i);
pthread_detach(threads_id[i - 1]); // 线程分离
sleep(2);
}
void* ret = nullptr;
// 一个个等
// for(int i = 0; i <= 4; i++)
// {
// pthread_join(threads_id[i], &ret);
// cout << "等到子线程" << *(int*)ret << endl;
// }
cout << "所有子线程都等到了,主线程退出" << endl;
return 0;
}
通过:ps -aL
我们可以看到线程的状态
展示部分(一边创建的时候,可能会有一边退出的)
LWP:轻量级进程的编号,CPU的调度单位(内核层用来标识轻量级进程)
三,用户层与内核层的“线程”
- 对于Linux,线程是用户态的概念,通过封装Linux的轻量化进程到
pthread
库实现 pthread
库底层通过封装系统调用clone
来与内核交互
首先,pthread
是一个库,也是一个文件,它会通过mmap
映射到进程地址空间上。
其次,库中每个线程有TCB
(语言层),并用链表或者其他数据结构组织。(类似PCB)
在这个TCB
里,主要有三个重要的结构:
struct pthread
:(线程控制块的核心)其中包括:用户层属性 + 内核映射 …- 线程局部存储:用来存储线程希望“专有”的变量(只能存储内置类型和部分指针)
- 线程独立的栈结构:存放该线程执行时产生的临时数据
1. 原码解析
glibc-2.4
中pthread
源码相关内容(只看重点):
// 线程的属性(用 struct pthread_attr 记录)
const struct pthread_attr *iattr = (struct pthread_attr *)attr;
// 指向struct pthread的起始地址(虚拟的)
// 这玩意就是我们调用pthread_creat 得到的线程ID
struct pthread *pd = NULL;
// 根据 iattr 中的栈属性(如 stacksize)分配线程栈
// 同时分配并初始化 struct pthread 结构体
// pd 存储返回新分配的 TCB 地址(同时也就是 struct pthread的起始地址)
int err = ALLOCATE_STACK(iattr, &pd);
// 记录线程要执行的函数的入口和参数
pd->start_routine = start_routine;
pd->arg = arg;
// 把线程ID存入newthread,newthread指向线程的 TCB
*newthread = (pthread_t)pd;
// 检查是否分离
bool is_detached = IS_DETACHED(pd);
// 创建一个进程,内部封装 clone
err = create_thread(pd, iattr, STACK_VARIABLES_ARGS);
struct pthread_attr
- 用于用户在创建线程前配置线程属性的结构体(如栈大小、分离状态等)
- 属于用户空间的 API,用户可以直接操作
- 只在调用
pthread_create()
时起作用
关键属性:
...
int flags; // 存储线程的各种属性标志位,其中包括分离状态
/* Stack handling. */
void *stackaddr; // 栈的起始地址
size_t stacksize; // 栈的大小
...
struct pthread
TCB的核心,(类似文件的FILE
结构体)
// 线程的 LWP(CPU的调度单位)
pid_t tid;
// 线程所属的进程的PID
pid_t pid;
// 存放进程函数退出的返回值
void *result;
// 用户指定的线程函数入口和参数
void *(*start_routine) (void *);
void *arg;
// 线程自己的栈和⼤⼩
void *stackblock; // 指向栈
size_t stackblock_size;
- 当我们创建一个线程,线程通过用户提供的函数入口
start_routine
去执行对应的代码 - 产生的临时数据存放在用户提供的独立的栈中
- 线程运行完毕,返回值就是
void*
, 返回值会被拷贝到result
- 线程退出后,结构体本身和线程栈不会立即释放
- 所以我们要用
pthread_join
等待,并且join
获取线程退出信息时,就是读取该结构体
create_thread
// 封装的clone
...
int res = do_clone(pd, attr, clone_flags, start_thread,
STACK_VARIABLES_ARGS, stopped);
do_clone
...
if (ARCH_CLONE(fct, STACK_VARIABLES_ARGS, clone_flags,
pd, &pd->tid, TLS_VALUE, &pd->tid) == -1)
ARCH_CLONE __clone
...
movl $SYS_ify(clone),%eax // 获取系统调⽤号
...
syscall // 陷⼊内核(x86_32是int 80),内核创建轻量级进程
...
- 所以,在创建线程的时候,其实就是在
pthread
库内部,创建好描述线程的结构体对象struct pthread
,填充属性(用户层)struct pthread
通常在线程栈的顶部高地址端(也就是在TCB的前面)
- 然后由系统创建好线程栈(通过
mmap
分配) - 调用
clone
,让内核创建轻量级进程,并传入回调函数和参数(系统层) - 其实,库提供的无非就是未来操作线程的API,通过属性设置线程的优先级之类,而真正调度的过程,还是内核来的
2. 线程栈
- 我们在传递线程栈的起始地址的时候,传递的是高地址,因为线程栈在主进程的堆上开辟,堆向下增长
- 线程栈的空间创建好以后就是固定的,大小为页大小的整数倍
- 它其实是在进程的地址空间中
map
出来的⼀块内存区域
3. 线程局部存储
- 如果我们定义一个全局变量,然后有两个线程,线程
a
对变量进行修改,线程b
读取,则b
是能读到a
的修改的。(此时变量是存储在进程的已初始化数据区的) - 但是如果我们在变量加
__thread
,则可以引导编译器:把变量的存储位置改到线程的局部存储区。 - 这时候,
a
对变量的修改,b
就看不到(虽然两进程访问的变量名相同,但是访问的实际是不同的虚拟地址) - 注意:线程局部存储只能存储内置类型和部分指针
四,模拟封装线程库
mythread.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <functional>
#include <stdlib.h>
#include <string>
namespace tr
{
int cnt = 1; // 设计一个计数器
class Mythread
{
private:
static void *start_routine(void *obj) // obj 是 this指针
{
// 类内成员函数有this指针,无法传入pthread_create
// 所以设计static成员函数(接受this),来回调要传入的方法
Mythread *self = static_cast<Mythread *>(obj);
// 在这里往线程局部存储存入_name,以便在类外能够获取
pthread_setname_np(self->_tid, self->_name.c_str());
self->_func(); // 回调
return nullptr;
}
public:
using func_t = std::function<void()>; // 接受一个无参无返回值的函数的回调
Mythread(bool enablejoin, func_t func) // 构造函数是在主线程执行的
: _enablejoin(enablejoin), _running(true), _func(func)
{
int ret = pthread_create(&_tid, nullptr, start_routine, this);
if (ret != 0)
{
perror("pthread_create");
exit(EXIT_FAILURE);
}
else
{
_name = "thread " + std::to_string(cnt++);
std::cout << _name << "创建成功" << std::endl;
}
if (!_enablejoin)
{
pthread_detach(_tid);
}
}
void Detach()
{
if (_enablejoin)
{
pthread_detach(_tid);
}
_enablejoin = false;
}
void Cancel()
{
if (_running)
{
pthread_cancel(_tid);
}
_running = false;
_enablejoin = false;
}
void Join() // 只能 join 自己
{
if (_enablejoin)
{
int ret = pthread_join(_tid, nullptr);
if (ret != 0)
{
perror("pthread_join");
exit(EXIT_FAILURE);
}
else
{
std::cout << _name << "被成功join" << std::endl;
}
}
else
{
std::cout << _name << "已经分离, 不能被join" << std::endl;
}
}
~Mythread()
{
}
private:
pthread_t _tid; // 用户线程 ID
bool _enablejoin;
bool _running;
std::string _name;
func_t _func; // 该线程要执行的函数
};
}
Main.cpp
#include "MyThread.hpp"
#include <unistd.h>
void func1()
{
sleep(1); // 等一下名字设置
char name[256];
pthread_getname_np(pthread_self(), name, sizeof(name));
std::cout << name << "任务执行完毕" << std::endl;
}
void func2()
{
sleep(1);
char name[256];
pthread_getname_np(pthread_self(), name, sizeof(name));
std::cout << name << "任务执行完毕" << std::endl;
}
int main()
{
pthread_setname_np(pthread_self(), "main_thread");
tr::Mythread t1(true, func1);
tr::Mythread t2(true, func2);
// t1.Detach();
// t1.Cancel();
t1.Join();
t2.Join();
sleep(3); // 让主线程慢一点退出
std::cout << "执行完毕" << std::endl;
return 0;
}
运行效果
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!