目录
🐼回顾
Linux不需要有线程的概念,只存在轻量化进程的概念!lwp(Light Weight Process)。但是在用户视角,只认进程和线程,为了方便用户使用,因此Linux在轻量化进程系统调用clone等的基础上,封装了一个原生线程库:pthread库。提供线程接口给用户使用!
🐼多线程共享同一虚拟地址空间
我们上节课说过**,线程是进程内的一个执行流,共用进程的虚拟地址空间,所以如果变量定义在全局,所有线程都能看到, 现在会发现以前的进程间通信,它们让两个进程看到同一份代码和资源的方法都很复杂,但是线程天生就具有看到同一份资源的能力,因为能看到同一份虚拟地址空间,因此,线程间通信十分方便。**
🔹 全局变量共享
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include<string>
int gval = 10;
void* thread_routine(void* args)
{
std::string name = static_cast<const char*>(args);
while (true)
{
gval++;
printf("new thread create success,name:%s gavl:%d &gval:%p\n",name.c_str(),gval,&gval);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
while (true)
{
printf("main thread create success gavl:%d &gval:%p\n",gval,&gval);
sleep(1);
}
return 0;
}
现象:

🔹全局函数共享
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include<string>
int gval = 10;
std::string func()
{
return "我是另一个函数";
}
void* thread_routine(void* args)
{
std::string name = static_cast<const char*>(args);
while (true)
{
gval++;
printf("new thread create success,name:%s gavl:%d &gval:%p func():%s &func():%p\n",name.c_str(),gval,&gval,func().c_str(),func);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
while (true)
{
printf("main thread create success gavl:%d &gval:%p func():%s &func():%p\n",gval,&gval,func().c_str(),func);
sleep(1);
}
return 0;
}

🔹堆共享,逻辑上是可以共享
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include<string>
int gval = 10;
int *data;
std::string func()
{
return "我是另一个函数";
}
void* thread_routine(void* args)
{
data = new int(10);
std::string name = static_cast<const char*>(args);
while (true)
{
gval++;
// printf("new thread create success,name:%s gavl:%d &gval:%p func():%s &func():%p\n",name.c_str(),gval,&gval,func().c_str(),func);
printf("new thread... data:%d &data:%p\n",*data,data);
sleep(1);
}
}
int main()
{
pthread_t tid;
int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
(void)n;//避免警告
sleep(2);//保证了新线程先向要堆空间,主线程再出发
while (true)
{
// printf("main thread create success gavl:%d &gval:%p func():%s &func():%p\n",gval,&gval,func().c_str(),func);
printf("main thread... data:%d &data:%p\n",*data,data);
sleep(1);
}
return 0;
}

我们之前可以创建多进程,可以创建多线程吗?可以!
创建多少最佳呢?
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。(最佳是CPU个数*CPU核数)
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。(可以多一些)
🔹创建一批线程
cpp
#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <pthread.h>
int gval = 10;
int *data;
std::string func()
{
return "我是另一个函数";
}
void *thread_routine(void *args)
{
// 堆空间,就是该线程申请的, 往往只
// // 需要让对应的线程知道这部分堆空间起始虚拟地址,就可以叫做这个对是该线程拥有的
// int *data = new int(10);
std::string name = static_cast<const char *>(args);
int cnt = 5;
while (cnt--)
{
// gval++;
// printf("new thread create success,name:%s gavl:%d &gval:%p func():%s &func():%p\n",name.c_str(),gval,&gval,func().c_str(),func);
// printf("new thread... data:%d &data:%p\n", *data, data);
printf("new thread is runing name is:%s\n", name.c_str());
sleep(1);
}
return nullptr; // 表示线程退出
}
int main()
{
std::vector<pthread_t> tids;
for (int i = 0; i < gval; i++)
{
pthread_t tid;
// 格式化线程名字
// char buff[64];//bug// 这样对线程初始化名字问题是因为线程的运行顺序不确定,buff是一个共享资源,导致错乱
char *buff = new char[64];
snprintf(buff, 64, "thread-%d", i);
int n = pthread_create(&tid, nullptr, thread_routine, (void *)buff);
(void)n; // 避免警告
tids.emplace_back(tid);
// printf("main thread create success:%s\n",buff);
}
sleep(1);
// sleep(2); // 保证了新线程先向要堆空间,主线程再出发
for (auto &tid : tids)
printf("main create a new thread, new thread id is : 0x%lx\n", tid);
for (auto &tid : tids)
pthread_join(tid, nullptr);
printf("main thread end...\n");
// while (true);
// while (true)
// {
// // printf("main thread create success gavl:%d &gval:%p func():%s &func():%p\n",gval,&gval,func().c_str(),func);
// printf("main thread... data:%d &data:%p\n",*data,data);
// sleep(1);
// }
// pthread_join(tid,nullptr);
return 0; // 主线程退出了,不管其他线程有没有运行完,都要退出了,因为要释放资源了!
}

一个线程死了,其他线程都崩溃了。因为整个进程崩溃

🔹给多线程添加任务、主线程等待子线程,并且获取退出信息。
cpp
#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <pthread.h>
void *thread_routine(void *args)
{
std::string name = static_cast<const char *>(args);
int cnt = 5;
while (cnt--)
{
printf("new thread is runing name is:%s,id is: 0x%lx\n", name.c_str(), pthread_self());
sleep(1);
}
//线程退出
// exit(0);//表示进程直接退出
// return nullptr; // 新线程退出方式1
// pthread_exit(0); // 新线程退出方式2
// pthread_cancel(pthread_self());//可以这么退出,但是实际不这么用
// return (void*)10;
// 方法3
// pthread_exit((void*)0);
// pthread_exit((void*)1);
// pthread_exit((void*)2);
// pthread_exit((void*)3);
pthread_exit((void*)4);
// return (void*)10; // 字面值,int,4 , "hello world!", char *str = "hello";
}
int main()
{
pthread_t tid;
int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread-1");
(void)n; // 避免警告
printf("main thread create success , main thread id is: 0x%lx,new thread id is: 0x%lx\n", pthread_self(), tid);
sleep(2);
// printf("cancel new thread...\n");
// pthread_cancel(tid);// 方法三本来就是main取消其他线程的---最佳实践
// 得知新线程执行的结果:代码跑完,结果对还是不对! 退出信息 = 信号 + 退出码,难道,多线程这里,不考虑所谓的异常吗??
// 不用考虑,因为没机会考虑!!!!
void *ret;
int m = pthread_join(tid, &ret); // int pthread_join(pthread_t thread, void **retval);
if (m == 0)
{
printf("pthread_join success,m: %d,ret:%lld\n", m, (long long)ret);
}
sleep(3);
printf("main thread end...\n");
return 0;
}

🐼各个接口的注意事项
首先,如果想使用pthread库,在编译时链接-lpthread库和你的程序进行编译。因为pthread库是Linux提供的第三方库。
✅ pthread_create的最后一个参数是void*类型的,这就给我们很大的拓展性和灵活性,所以最后一个参数谁说只能传内置类型,传对象也可以,比如:
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
class Task
{
public:
Task() {}
Task(int x, int y) : _x(x), _y(y) {}
void Div()
{
if (_y == 0)
{
_code = 1;
return;
}
_result = _x / _y;
}
void Print()
{
std::cout << "result: " << _result << "[" << _code << "]" << std::endl;
}
~Task() {}
private:
int _x;
int _y;
int _code;
int _result;
};
void *thread_routine(void *args)
{
sleep(1);
// std::string name = static_cast<const char *>(args);
// std::cout << "我是新线程:" << name << std::endl;
Task *t = static_cast<Task *>(args);
t->Div();
// pthread_detach(pthread_self());
return t;
}
int main()
{
pthread_t tid;
Task *t = new Task(10, 0);
pthread_create(&tid, nullptr, thread_routine, t);
std::cout << "我是主线程" << std::endl;
sleep(2);
// pthread_detach(tid);//线程分离不需要join!!!
// while (true)
// ;
void *ret;
int m = pthread_join(tid, &ret);
// Task *result = (Task *)ret;
// result->Print();
std::cout << m << std::endl;
if (m == 0)
{
Task *result = static_cast<Task *>(ret);
result->Print();
std::cout << "join success" << " " << (long long int)ret << std::endl;
}
else
{
std::cout << "m:" << strerror(m) << std::endl;
}
return 0;
}
**✅**我们可以使用pthread_self得到当前线程的ID,pthread 库也是通过内核提供的系统调用(例如clone)来创建线程的,而内核会为每个线程创建系统全局唯⼀的"ID"来唯⼀标识这个线程。
我们可以使用ps -aL | head -1查看线程信息
LWP(Light Weight Process,轻量级进程)是 Linux 内核中的线程实现方式,它得到的是真正的线程ID。之前使用 pthread_self得到的这个数实际上是一个地址,在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID、线程栈、寄存器等属性。
在 ps -aL得到的线程ID中,有一个线程ID和进程ID相同,这个线程就是主线程。主线程的栈在虚拟地址空间的栈上,而其他线程的栈是在共享区(堆栈之间) ,因为 pthread系列函数都是 pthread 库提供给我们的,而 pthread 库是在共享区的。所以 ,除了主线程之外的其他线程的栈都在共享区。
✅如果需要只终止某个线程而不终止整个进程,可以有三种方法:
从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
线程可以调用pthread_ exit终⽌自已(pthread_exit(pthread_self())。
⼀个线程可以调用pthread_ cancel终止同⼀进程中的另⼀个线程。
✅默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。 如果不关心线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源,我们可以使用pthread_detach()来分离某个线程,表示该线程如果无异常运行完会自动退出,不需要主线程再join,和SIGCHLD原理有点类似 。joinable和分离是冲突的,一个线程不能既是joinable又是分离的。分离线程,主线程如果detach指定线程,表示对他不关心:
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
class Task
{
public:
Task() {}
Task(int x, int y) : _x(x), _y(y) {}
void Div()
{
if (_y == 0)
{
_code = 1;
return;
}
_result = _x / _y;
}
void Print()
{
std::cout << "result: " << _result << "[" << _code << "]" << std::endl;
}
~Task() {}
private:
int _x;
int _y;
int _code;
int _result;
};
void *thread_routine(void *args)
{
sleep(1);
// std::string name = static_cast<const char *>(args);
// std::cout << "我是新线程:" << name << std::endl;
Task *t = static_cast<Task *>(args);
t->Div();
// pthread_detach(pthread_self());
return t;
}
int main()
{
pthread_t tid;
Task *t = new Task(10, 0);
pthread_create(&tid, nullptr, thread_routine, t);
std::cout << "我是主线程" << std::endl;
sleep(2);
pthread_detach(tid);//线程分离不需要join!!!
while(true);
void *ret;
// int m = pthread_join(tid, &ret);
// // Task *result = (Task *)ret;
// // result->Print();
// std::cout<<m<<std::endl;
// if (m == 0)
// {
// Task *result = static_cast<Task *>(ret);
// result->Print();
// std::cout << "join success" << " " << (long long int)ret << std::endl;
// }
// else
// {
// std::cout << "m:" << strerror(m) << std::endl;
// }
return 0;
}
🐼如何理解tid,以及传参问题
下面我们谈谈pthread_t究竟是什么类型,为什么pthread_create第一个参数就是tid。pthread_join是如何获取到线程的返回值的。
✅首先我们要很清楚,既然线程有不同的工作状态,这么多线程,os一定要有一个struct tcb{}来管理这些线程,由于pthread库是共享库,并且每一个线程都会维护自已的栈,上下文,而我们创建线程,本质上是在共享库中malloc,在共享库内部,帮助我们维护了一个线程对象。
pthread_create 调用后,内核会分配一个 LWP(轻量级进程,即线程),pthread 库在用户态维护一个线程控制块(TCB),存储线程的栈、寄存器、状态等信息。而反应给用户的就是一个地址,为了让用户找到这块线程,所以才有了pthread_t,所以pthread_t,本质就是⼀个进程地址空间上的⼀个地址.创建线程pthread库源码部分重要字段:
cpp
// Linux传说的phread库源码
// 重点2:传说中的原⽣线程库中的⽤来描述线程的tcb
struct pthread *pd = NULL;
// 把pd(就是线程控制块地址)作为ID,传递出去,所以上层拿到的就是⼀个虚拟地址
*newthread = (pthread_t)pd
/* Thread ID - which is also a 'is this thread descriptor (and
therefore stack) used' flag. */
pid_t tid;
/* Process ID - thread group ID in kernel speak. */
pid_t pid;
// 线程⾃⼰的栈和⼤⼩
void *stackblock;
size_t stackblock_size;
syscall // 陷⼊内核(x86_32是int 80),要求内核创建轻量级进程
在创建线程时,就是在共享库帮我们维护了一个线程对象,我们将tid等创建信息写入到了struct tcb,而库让我们可以通过tid来找到这块地址,所以tid是一个输出型参数。
✅理解了这点,那如果主线程关心新线程的返回值,pthread_join()第二个参数是如何获取到新线程的返回值的?
**首先,新线程返回值,一定是向维护的那块虚拟地址的struct tcb的某个字段中写入,我们通过二级指针,指向这个字段,自然就能获取到了!**pthread库源码字段:
cpp
/* The result of the thread function. */
// 线程运⾏完毕,返回值就是void*, 最后的返回值就放在tcb中的该变量⾥⾯
// 所以我们⽤pthread_join获取线程退出信息的时候,就是读取该结构体
// 另外,要能理解线程执⾏流可以退出,但是tcb可以暂时保留,这句话
void *result;
所以pthread库让我们用户可以通过pthread_create的第一个参数找到库维护的那块线程对象所占的虚拟地址。如图所示:


最后再总结一下,pthread_t 的值通常是 TCB 的地址(用户态视角),而非内核的 LWP ID。
在 Linux 中,pthread_t 是用户态线程标识符,通过 pthread_self()获取,本质是线程库管理的 TCB 地址;而 LWP(TID)是内核态线程 ID,通过 syscall(SYS_gettid) 获取,代表内核调度的真实线程 ID。两者分别用于用户态和内核态的线程管理。
🐼封装线程库
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
// 1. 后面加上拷贝构造和移动构造函数
// 2. _is_running 设置为临界资源
// 3. Debug 策略换成日志
namespace lsg
{
using func_t = std::function<void()>;
#define GET_LWPID() syscall(SYS_gettid)
class Thread
{
public:
Thread(const std::string &name, func_t func)
{
std::cout << _name << ": create success" << std::endl;
}
Thread(const Thread &) = delete;
Thread &operator=(const Thread &) = delete;
static void *routine(void *args)
{
Thread *self = static_cast<Thread *>(args);
self->_is_running = true;
self->lwpid = GET_LWPID();
self->_func();
pthread_exit((void *)0);
}
bool Start()
{
// 安全检查
if (!_func)
{
std::cerr << "Thread function is empty!" << std::endl;
return false;
}
int n = pthread_create(&_tid, nullptr, routine, this);
if (n == 0)
{
std::cout << _name << ": run success" << std::endl;
return true;
}
else
{
std::cerr << "pthread_create error: " << strerror(errno) << std::endl;
return false;
}
}
bool Join()
{
if (!_is_running)
return false;
int n = pthread_join(_tid, nullptr);
_is_running = false;
if (n == 0)
{
std::cout << _name << ": join success" << std::endl;
return true;
}
else
{
std::cerr << "join error: " << strerror(errno) << std::endl;
return false;
}
}
bool Die()
{
if (!_is_running)
return false;
int n = pthread_cancel(_tid);
if (n == 0)
{
std::cout << _name << ": cancel success" << std::endl;
return true;
}
else
{
std::cerr << "cancel error: " << strerror(errno) << std::endl;
return false;
}
}
~Thread()
{
if (_is_running)
{
std::cerr << "WARNING: Thread " << _name << " is still running!" << std::endl;
pthread_detach(_tid);
}
}
private:
bool _is_running; // 临界资源。主线程和子线程
std::string _name;
pthread_t _tid;
pid_t lwpid;
func_t _func;
};
}
需要注意的一点就是,由于pthread_create的回调函数参数是void*args,但是作为类的成员函数,隐藏this指针,所以我们将其实现为类的静态成员方法,为了访问其私有成员属性,我们将this作为参数传递进去.