目录
[一 Linux线程控制](#一 Linux线程控制)
[1 POSIX线程库](#1 POSIX线程库)
[2 创建线程](#2 创建线程)
[3 线程终止](#3 线程终止)
[4 线程等待](#4 线程等待)
[5 线程分离](#5 线程分离)
一 Linux线程控制
1 POSIX线程库
Linux中使用线程必须使用POSIX线程库
Linux只会提供创建轻量级进程的接口,必然要把接口做封装,体现线程的概念;所以我们使用的是基于POSIX库的用户级线程
pthread库叫做原生线程库,只要是Linux系统就必须有pthread库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以 pthread_ 打头的。
-
要使用这些函数库,需要引入头文件 <pthread.h>
-
链接这些线程函数库时要使用编译器命令的**-lpthread** 选项
2 创建线程
cpp
功能:创建⼀个新的线程
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
Linux中用户级线程:LWP=1:1
线程ID本质是一个虚拟地址
线程在库里要被管理-->先描述,再组织;用struct thread表示一个打开的线程
可以给线程传递字符串,那么也可以传类,结构体变量等等;相当于给它派任务
pthread_self:获取调用线程自己的ID

cpp
#include <iostream>
#include <vector>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
// 定义线程名字缓冲区大小
const int gsize = 64;
// 线程入口函数
void *threadrun(void *args)
{
// 转换参数类型,获取线程名字
std::string name = static_cast<const char *>(args);
while (true)
{
// 打印:用户层线程ID(tid)、进程ID(pid)、线程名字
// 注意:%lx 对应 pthread_t (通常是unsigned long)
printf("我是一个新线程: tid: 0x%lx, pid: %d, name : %s\n",
pthread_self(), getpid(), name.c_str());
sleep(1); // 每隔1秒打印一次
}
return nullptr; // 线程退出
}
// 主函数
int main(int argc, char *argv[])
{
// 检查命令行参数
if (argc != 2)
{
std::cout << "用法: " << argv[0] << " <线程数量>" << std::endl;
return 1;
}
// 将输入的字符串参数转换为整数
int num = std::stoi(argv[1]);
// 用于存储所有创建的线程ID
std::vector<pthread_t> tids;
// 批量创建线程
for (int i = 0; i < num; i++)
{
pthread_t tid;
// 创建线程名字缓冲区
// 注意:这里声明在循环内,意味着每个线程都有自己独立的栈空间
// 解决了线程共享栈内存导致的乱码问题
char threadname[gsize];
// 格式化线程名字 (如: thread-1, thread-2)
snprintf(threadname, sizeof(threadname), "thread-%d", i + 1);
// 创建线程
// threadrun: 线程执行的函数
// (void *)threadname: 传递给线程的参数 (线程名字)
pthread_create(&tid, nullptr, threadrun, (void *)threadname);
// 将新创建的线程ID存入vector
tids.push_back(tid);
// 此处sleep(1)是为了让线程有足够的时间启动并打印,防止主线程过快循环覆盖数据
// 实际生产中通常不需要sleep,而是通过管理线程ID数组来等待
sleep(1);
}
// 主线程死循环(防止进程提前退出)
// 因为子线程是分离状态吗?不,这里是join等待模式
// 为了演示,我们让主线程一直挂起
while (true)
{
sleep(1);
}
return 0;
}
pthread_create 创建线程成功后,主线程会继续向下执行,新线程则执行对应的线程函数 threadrun 。
当我们在主线程的 for 循环中,用栈上的局部数组 char threadname[gsize] 存储线程名(如 thread-1),并将其作为参数传递给新线程时:
数组名传参本质是传递缓冲区的地址(如 0x12345678),而非拷贝字符串内容。
新线程的 args 参数会指向主线程栈上的这块缓冲区,而非拥有独立副本。
由于线程调度由操作系统的大 O1 调度算法决定,主线程和新线程的执行顺序完全不确定:
如果主线程执行速度更快,会在新线程读取缓冲区前,就用下一轮循环的 snprintf 把缓冲区内容覆盖为 thread-2;
最终导致新线程拿到的是被覆盖后的新名字,出现 "老线程拿到新线程名字" 的问题
threadname 这段缓冲区之所以会被所有线程共享,核心原因是:
同一进程内的所有线程共享同一个虚拟地址空间,原则上进程内的所有数据(包括主线程栈上的临时变量)都是共享资源,只要拿到地址就能访问。
当多个线程并发访问同一份共享资源,且一个线程的操作会影响另一个线程的执行结果时,这份代码就存在线程安全问题。
因为共享资源的访问,一个线程可能会影响另一个线程的指向,此时我们叫做这份代码具有线程安全问题
解决方案
方案 1:临时加 sleep(1)
在 pthread_create 后加 sleep(1),可以让主线程等待新线程启动并读取完缓冲区后,再执行下一轮循环,从而避免缓冲区被覆盖。但这只是临时规避,并非根本解决,且会影响程序性能,不适合生产环境。
方案 2:堆上分配独立空间
如果不想使用 sleep,可以在每次创建线程时,用 new 在堆上为每个线程分配独立的缓冲区:
cpp
char* threadname = new char[gsize];
这样每个线程都拥有自己独立的堆内存空间,互不干扰,从根本上解决了栈缓冲区被覆盖的线程安全问题
3 线程终止
线程的返回值是void*,所以返回的时候,函数也需要通过return返回某种返回值
函数跑完,线程自然就结束,退出
退出方法1:调用return,表示线程退出
main中return 0 表示主线程退出,进程退出
exit是用来终止进程的,不能用来终止线程,多线程中任意一个线程调用exit,都表示整个进程退出
| 位置 | 语句 | 效果 |
|---|---|---|
| 线程函数(threadrun) | return |
只退出这个线程,其他线程继续跑 |
| main 函数 | return |
退出主线程 ⇒ 整个进程退出 |
| 任意线程 | exit() |
整个进程退出(不能退出线程!) |
退出方法2:库函数pthread_exit
cpp
void pthread_exit(void *retval);
void *retval:表示线程退出的返回值,可以是 nullptr / 整数 / 指针;这个值会被 pthread_join 接收
在unbuntu 24.04下,pthread_exit只能用来终止新线程,终止主线程没用
线程处理函数,除了要交给他任务去处理,任务处理的怎么样,也要告诉主线程;所以线程有一个返回值
怎么拿到线程退出的返回值?
线程退出时 把返回值交出来
主线程用 pthread_join 去拿
拿出来后 强转类型 就能用
4 线程等待
主线程创建了新线程,也要能等待新线程
等待线程函数:pthread_join
cpp
int pthread_join(pthread_t thread, void **retval);
pthread_t thread
作用:要等待的线程 ID
就是你 pthread_create 出来的那个 tid
void retval
作用: 用来接收线程退出的返回值
线程返回的值会通过这个参数写回来
不需要返回值 → 填 nullptr
pthread_join 第二个参数是一个输出型参数,用来拿线程的返回值!
(1)为什么要等?
如果主线程不等待新线程,就会造成类似僵尸进程的问题-->导致内存泄漏
通过等待,得到线程退出的退出信息
(2)如何等待?
主线程调用pthread_join;由第二个参数,得到线程的退出信息
cpp
void *threadrun(void *args)
{
// ... 线程执行逻辑 ...
return (void*)10; // 线程退出,返回值 10
}
int main()
{
pthread_t tid;
// ... 创建线程 ...
void *ret = nullptr; // 定义接收返回值的变量
pthread_join(tid, &ret); // 调用 join,传入 ret 的地址 &ret
printf("ret code: %lld\n", (long long)ret); // 强转后打印,输出 10
return 0;
}
线程的返回值,就是return后面的数字:return (void*)10; 相当于把数字10写到指针变量里面
在pthread库里面,会单独有一个空间,是单独存在的,它会把当前线程结束时所对应的退出数据或void*类型变量。直接写道这个空间里面,一个线程一个,互相不影响
之后主线程想通过函数获取这个保存在库变量里面的值 ,这个值是void*类型的,把void*类型变量拿出来,就得定义void**(void类型没有办法定义变量,它的大小是不确定的,void*可以定义变量。因为它的类型是明确的,是指针,32位平台下是4字节,64位平台下是8字节)
Eq: int* p = (int*)20; 指针变量也是变量,指针和指针变量是完全不一样的(指针变量更强调它的空间)
两者是完全不同的东西:指针是地址,指针变量是存储地址的容器,就像「门牌号」和「写着门牌号的纸条」的区别。
cpp
int a = 10;//这里强调它是变量,对应空间
int b = a;//强调它的数值是10
同一个变量,有时被当成左值(空间),有时是右值(数值(
cpp
int* p=(int*)20;这里的p是指针变量
int* q=p;这里的p指针
怎样通过函数获取void*里面的内容?
传递二级指针
如果线程异常了,pthread_join异常无法正确返回,没有意义;因为任何线程异常,进程都会退出,所以pthread_join不关心异常
如果新线程不退出,join就会一直等待下去
退出方法3(不推荐):一个线程可以调用pthread_cancel终止同一进程的另一个线程
cpp
功能:取消⼀个执⾏中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:
thread:线程ID
返回值:成功返回0;失败返回错误码
如果一个线程是被取消的,那么它的退出码是-1
线程的返回值不一定是整数,而可以是类,结构体....
如果·我们未来不想join阻塞等待新线程呢?
5 线程分离
如何进行线程分离?
pthread_detach:让一个线程从主线程中分离
cpp
int pthread_detach(pthread_t thread);

我们把线程创建出来,默认改线程是必须被join的,如果不join,就会造成类似僵尸进程的问题;按这种情况,线程的状态叫做joinable的
如果一个线程创建出来必须要等待,那么这个线程就是joinable的;如果不想等待,也不关心线程的退出情况,那么可以把线程设置成分离状态
一个线程如果创建了,它也可以自己把自己设置成分离:
cpp
pthread_detach(pthread_self());
线程分离的理解:
在七八十年代,父亲和儿子分家,可以由父亲向儿子提,也可以儿子向父亲提;体现在代码的层面上,可以新线程自己分离,也可以主线程分离新线程,主线程就不再关心新线程
joinable和分离式冲突的,一个线程不能既是joinable的也是分离的