目录
- 一、线程创建
- 二、线程终止
- 三、线程等待
- 四、线程分离
- 五、线程ID及进程地址空间布局
- [六、C++ 多线程](#六、C++ 多线程)

个人主页:矢望
个人专栏:C++、Linux、C语言、数据结构、Coze-AI、MySQL
一、线程创建
1.1 创建线程

| 参数 | 类型 | 是否输出 | 作用 | 示例 |
|---|---|---|---|---|
thread |
pthread_t * |
输出参数 | 成功创建线程后,存储新线程的ID(用于后续操作如 pthread_join、pthread_cancel) |
pthread_t tid; → &tid |
attr |
const pthread_attr_t * |
输入参数 | 设置线程属性(如栈大小、调度策略、分离状态等);传 NULL 表示使用默认属性 |
NULL 或 &attr |
start_routine |
void *(*)(void *) |
输入参数 | 线程入口函数指针,线程创建后从此函数开始执行;函数签名固定为 void* func(void*) |
&thread_func |
arg |
void * |
输入参数 | 传递给入口函数的参数,可以是任意类型指针;无参数时传 NULL |
&my_data 或 NULL |
代码:
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
void* routine(void* args)
{
const char* name = static_cast<const char*>(args); // 对 args 进行类型强转
while(true)
{
printf("新线程正在运行, name: %s, pid: %d\n", name, getpid());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"Thread-1");
while(true)
{
printf("主线程正在运行,pid: %d\n", getpid());
sleep(1);
}
return 0;
}
编译运行:

如上,创建出了新线程。
我们创建出新线程之后,顺便也在代码中得到了新线程的tid,这个内容是什么呢?我们打印看一下,是LWP吗?

如上图,我们打印出来发现,这并不是LWP,是一个很大的数字。这在在 Linux 上是用户态线程库内部数据结构的地址 。所以我们可以给主线程和新线程都按照地址打印一下。

pthread_self() 返回当前线程的 pthread_t 标识, 获取的是用户态线程 ID。在 Linux 上本质是用户态线程控制块的堆内存地址,用于线程库内部快速访问线程私有数据。
cpp
void* routine(void* args)
{
const char* name = static_cast<const char*>(args); // 对 args 进行类型强转
while(true)
{
printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name, pthread_self(), getpid());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"Thread-1");
printf("新线程 tid: 0x%lx\n", tid);
while(true)
{
printf("主线程正在运行, tid: 0x%lx, pid: %d\n", pthread_self(), getpid());
sleep(1);
}
return 0;
}
编译运行:

1.2 创建多线程
代码:
cpp
void* routine(void* args)
{
std::string name = static_cast<const char*>(args); // 对 args 进行类型强转
while(true)
{
printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name.c_str(), pthread_self(), getpid());
sleep(1);
}
}
int main()
{
int num = 10;
for(int i = 1; i <= num; i++)
{
pthread_t tid;
// 构建进程名称
char thread_name[64];
snprintf(thread_name, sizeof(thread_name), "Thread-%d", i);
pthread_create(&tid, nullptr, routine, thread_name);
// 只有主线程可以执行到这里
sleep(1);
}
while(true)
{
printf("主线程正在运行, tid: 0x%lx, pid: %d\n", pthread_self(), getpid());
sleep(1);
}
return 0;
}
在上面代码中,只有主线程可以执行创建线程的for循环,其它的线程,一旦被创建就会去执行routine方法。
编译运行:

如上图,我们创建了10个线程。
并且这10个线程,这10个执行流都在执行routine函数,多个执行流同时进入一个函数,所以这个函数被重入了!又凸显了线程资源共享(代码共享)的特点。
如果我在全局定义一个变量,在新线程中修改变量,而主线程只读取变量,你会发现主线程的变量变化了:
cpp
int gnum = 100;
void* routine(void* args)
{
std::string name = static_cast<const char*>(args); // 对 args 进行类型强转
while(true)
{
printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name.c_str(), pthread_self(), getpid());
sleep(1);
gnum++;
}
}
int main()
{
int num = 1;
//...
while(true)
{
printf("主线程正在运行, tid: 0x%lx, pid: %d, gnum: %d\n", pthread_self(), getpid(), gnum);
sleep(1);
}
return 0;
}
编译运行:

异常问题
- 如果进程内部的一个线程出异常了,这个进程会怎样?
对代码进行修改,挑选一个线程让它发生异常,例如除0错误。
cpp
void* routine(void* args)
{
std::string name = static_cast<const char*>(args); // 对 args 进行类型强转
if(name == "Thread-6")
{
printf("Thread-6say#: 要出异常了!\n");
int a = 10;
a /= 0;
}
while(true)
{
printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name.c_str(), pthread_self(), getpid());
sleep(1);
gnum++;
}
}
编译运行:

如上,进程终止了。所以 任意线程异常崩溃,会导致整个进程挂掉!
- 并发问题
我们的代码中有一个问题,如果将创建线程循环中的sleep去掉就会显现,如下图:
cpp
for(int i = 1; i <= num; i++)
{
pthread_t tid;
// 构建进程名称
char thread_name[64];
snprintf(thread_name, sizeof(thread_name), "Thread-%d", i);
pthread_create(&tid, nullptr, routine, thread_name);
// 只有主线程可以执行到这里
// sleep(1);
}
编译运行:

如上图,通过tid我们可以发现,标注的是三个不同的线程,但是它们的线程名获取的是相同的!
这是因为循环中的thread_name的缓冲区中保存着线程名数据,但是它是所有线程共享的,所以当创建出一个新线程之后,这个线程就会拿到thread_name的地址。可是for循环进行很快,可能这个线程还没来得及进行拷贝数据,这个缓冲区中的数据就被更改了,所以拿到的数据就变化了,导致多个进程拿到了同一份数据,这种问题是由并发导致的数据不一致问题。
由于thread_name是线程共享的资源,所以我们不能这么使用,我们要给每一个新线程一份独立的数组。
cpp
for(int i = 1; i <= num; i++)
{
pthread_t tid;
// 构建进程名称
char* thread_name = new char[64];
sprintf(thread_name, "Thread-%d", i);
pthread_create(&tid, nullptr, routine, thread_name);
// 只有主线程可以执行到这里
// sleep(1);
}
编译运行:

如上图,这样就不会导致数据不一致问题了。
传参问题
pthread_create中的传递的函数是void*的,它是任意类型,所以它就可以传递很多类型,不止有字符串,例如传递类和结构体对象的地址,所以就可以给线程传递任务了!
现在我们写一个简单的加法类:
cpp
#pragma once
#include <string>
class Task
{
public:
Task(int x, int y)
:_x(x)
,_y(y)
{}
void Excute()
{
_result = _x + _y;
}
std::string Result()
{
return std::to_string(_x) + " + " + std::to_string(_y) + " = " + std::to_string(_result);
}
~Task(){}
private:
int _x;
int _y;
int _result;
};
然后,创建新线程时,就传递task任务:
cpp
void* routine(void* args)
{
Task* t = static_cast<Task*>(args); // 对 args 进行类型强转
t->Excute();
std::cout << t->Result() << std::endl;
return nullptr;
}
int main()
{
int num = 10;
srand((unsigned int)time(nullptr) ^ getpid());
for(int i = 1; i <= num; i++)
{
pthread_t tid;
int x = rand() % 50 + 1;
usleep(123); // 尽量让生成的两个数更随机
int y = rand() % 66 + 1;
Task* task = new Task(x, y);
pthread_create(&tid, nullptr, routine, task); // 传递任务
// 只有主线程可以执行到这里
// sleep(1);
}
while(true)
{
printf("主线程正在运行, tid: 0x%lx, pid: %d, gnum: %d\n", pthread_self(), getpid(), gnum);
sleep(1);
}
return 0;
}
上面代码中使用了随机数来生成两个加数。
编译运行:

如上图,这样线程就可以执行我们给的任务了!
二、线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数
return。这种方法对主线程不适用,从main函数return相当于调用exit。 - 线程可以调用
pthread_ exit终止自己。 - ⼀个线程可以调用
pthread_cancel终止同一进程中的另一个线程。
注意 :exit是终止整个进程的函数。

pthread_exit():终止调用它的线程。
使用示例:
cpp
void* routine(void* args)
{
Task* t = static_cast<Task*>(args); // 对 args 进行类型强转
t->Excute();
std::cout << t->Result() << std::endl;
pthread_exit(nullptr);
// return nullptr;
}
编译运行:

如上图,最终只剩下主线程了。
注意,如果主线程调用,主线程也会终止,但是进程不会退出。
cpp
void* routine(void* args)
{
Task* t = static_cast<Task*>(args); // 对 args 进行类型强转
t->Excute();
std::cout << t->Result() << std::endl;
while(true)
{
std::cout << "新线程还活着..." << std::endl;
sleep(1);
}
}
int main()
{
int num = 1; // 创建一个新线程
// ...
std::cout << "主线程退出..." << std::endl;
pthread_exit(nullptr);
while(true)
{
printf("主线程正在运行, tid: 0x%lx, pid: %d, gnum: %d\n", pthread_self(), getpid(), gnum);
sleep(1);
}
return 0;
}
编译运行:

如上,主线程终止,新线程继续运行。

pthread_cancel():向指定线程发送取消请求,使其终止。
我们可以让主线程收集新线程的tid,然后在后面使用tid终止新线程。
cpp
void* routine(void* args)
{
Task* t = static_cast<Task*>(args); // 对 args 进行类型强转
t->Excute();
std::cout << t->Result() << std::endl;
while(true)
{
std::cout << "新线程还活着..." << std::endl;
sleep(1);
}
}
int main()
{
int num = 1;
srand((unsigned int)time(nullptr) ^ getpid());
std::vector<pthread_t> tids;
for(int i = 1; i <= num; i++)
{
pthread_t tid;
// ...
// 收集 tid
tids.push_back(tid);
}
for(auto& e : tids)
{
pthread_cancel(e);
}
while(true)
{
printf("主线程正在运行, tid: 0x%lx, pid: %d, gnum: %d\n", pthread_self(), getpid(), gnum);
sleep(1);
}
return 0;
}
如上代码,新线程不会主动退出,如果新线程减少了就是主进程干的。
编译运行:

如上,新创建的线程全部被终止了。
不推荐这个函数终止线程,别的线程可能正在完成它的任务。另外如果新线程内调用pthread_cancel去终止主线程是终止不了的。
三、线程等待
新线程必须被主线程等待,否则会出现类似子进程那里的僵尸问题,主线程可以获取新线程的执行结果。
pthread_join 用于等待指定线程终止 ,并将该线程的退出状态(通过 pthread_exit 或 return 返回的 void* 值)存储到第二个参数中,同时回收线程资源。

如上图,第一个参数是要等待的目标线程id,第二个参数是输出型参数,二级指针,用于接收被等待线程的退出状态(即 pthread_exit 或 return 返回的 void* 值);如果为 NULL 则忽略退出状态。
返回值:等待成功返回0,失败返回错误码。
cpp
void* Routine(void* args)
{
std::string name = static_cast<const char*>(args); // 对 args 进行类型强转
while(true)
{
printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name.c_str(), pthread_self(), getpid());
sleep(3);
break;
}
return (void*)10;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, Routine, (void*)"Thread-1");
sleep(5);
// 1. 一般而言必须等待新线程退出,如果不等待,导致类似僵尸进程的问题
// 2. 为了获取新线程执行的结果
void *retval = nullptr;
int n = pthread_join(tid, &retval);
if(n == 0)
{
std::cout << "join success! " << (long long)retval << std::endl;
}
return 0;
}
如上代码中,我们创建出新线程,然后执行3s之后,让新线程退出,主线程获取它。
编译运行:

如上图,新线程获取之后,主线程等待新线程成功,拿到了新线程的退出码。
代码中,为什么直接获取的就是新线程的退出码呢?那退出信号呢?也就是说不需要进行异常分析吗? 任何线程出异常,进程直接终止!没有机会join成功,所以不需要在这里关心异常!只关心正常情况。
返回值类型
Routine的返回值类型可是void*的,谁规定只能返回整形类型的,我可以返回类对象等任意类型!
我们在前面不是实现了一个加法类吗,我就要让它执行加法任务,然后给我返回。
cpp
void* Routine(void* args)
{
Task* t = static_cast<Task*>(args); // 对 args 进行类型强转
std::cout << "新线程执行任务" << std::endl;
t->Excute(); // 执行任务
std::cout << "执行完毕!" << std::endl;
return (void*)t;
}
int main()
{
Task t(10, 20);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, (void*)&t);
sleep(5);
Task* task;
int n = pthread_join(tid, (void**)&task);
if(n == 0)
{
// 得到任务的执行结果
std::cout << "join success! " << task->Result() << std::endl;
}
return 0;
}
编译运行:

注意:如果thread线程被别的线程调用pthread_cancel异常终掉,再次等待线程成功之后,第二个参数retval所指向的单元里存放的是常数PTHREAD_CANCELED,这个值是-1。

四、线程分离
线程内部是不可以直接执行程序替换函数exec*的,这样会导致整个进程的代码和数据都被替换掉,但是可以fork+exec*。
主线程进行线程等待是阻塞等待的,如果想让线程全部自动结束掉,线程的执行结果并不关心,主线程不想等待线程,可以将目标线程设置成为分离状态。
pthread_detach 用于将指定线程标记为分离状态 ,意味着该线程终止时会自动释放其占用的系统资源(如线程栈、TCB等) ,无需也不能再用 pthread_join 等待和回收。

cpp
void* Routine(void* args)
{
std::string name = static_cast<const char*>(args); // 对 args 进行类型强转
int cnt = 5;
while(cnt--)
{
printf("新线程正在运行, name: %s, tid: 0x%lx, pid: %d\n", name.c_str(), pthread_self(), getpid());
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, Routine, (void*)"Thread-1");
std::cout << "主线程分离线程" << std::endl;
pthread_detach(tid); // 分离线程
sleep(3);
int* ret = nullptr;
int n = pthread_join(tid, (void**)&ret);
if(n == 0)
{
std::cout << "join success! " << n << std::endl;
}
else
{
std::cout << "join error! " << n << std::endl;
}
return 0;
}
如上代码,我们让主线程对新线程进行了分离,之后在进行线程等待的时候,我们就应该等待失败,然后拿到失败的错误码。
编译运行:

等待失败时候,主线程退出,整个进程退出。
扩充:新线程被分离之后,如果在线程内部发生了异常等问题,整个进程依然会被终止。
最佳实践:一旦线程要被设置为分离,主线程不能提前退,甚至主线程是个死循环!
五、线程ID及进程地址空间布局

如上图所示,pthread库也是库,所以它就要被映射到当前进程的虚拟地址空间以支持线程控制。
一个进程中的线程可以有多个,所以就线程需要被进程管理起来,例如描述线程的结构体struct Tcb{...}。如下图所示:

上图圈出来的就是线程描述结构体Tcb,之前说线程ID是一个地址,这个地址就是这个描述结构体的起始地址。
此外在线程描述结构体中有一个void *result的成员,这个里面存储的就是线程运行结束的返回值,我们调用pthread_join时,就是通过第一个参数找到描述结构体,然后第二个参数会将result中存放的值带出来 。

在创建线程的时候struct pthread :struct task_struct是1:1的,所以也叫做1:1式的用户级线程。前面那个用户级线程结构体是一个摆设,真正在内核中干活的是后面的PCB,当PCB执行完成之后,会将数据在pthread中写一份。
所以我们之前才会看到即使没有pthread_join线程,我们在查询的时候,依旧看不到类似僵尸线程的状态,因为task_struct确实被释放了,所以查不到;但是必须对线程进行等待是因为pthread这个描述结构体还在占用着空间,如果不进行等待会造成内存泄漏,这个内存泄漏是在库里面发生的。
| 结构体 | 所在层级 | 核心职责 | 生命周期管理 |
|---|---|---|---|
struct pthread |
用户态 (由pthread库在堆/mmap区域管理) |
存储线程的用户态上下文:pthread_t ID(即结构体自身地址)、线程栈地址、线程局部存储(TLS)、void *result 返回值等。 |
由 pthread_create 创建,必须由 pthread_join 或 pthread_detach 回收,否则会内存泄漏。 |
task_struct |
内核态 (内核管理) | 内核调度实体(LWP),存储内核上下文:进程PID、调度优先级、CPU状态、打开文件表等。 |
由 clone 系统调用创建,线程主动退出或被取消时,内核会立即回收此结构体。 |
Linux线程是"一个用户态的壳(struct pthread)包裹着一个内核态的核(task_struct)"。内核回收"核",而"壳"需要用户通过 pthread_join 显式回收。
线程局部存储
cpp
__thread int cnt = 0;
void* Routine(void* args)
{
std::string name = static_cast<const char*>(args); // 对 args 进行类型强转
while(true)
{
printf("new thread cnt: %d\n", cnt);
printf("new thread cnt: %p\n", &cnt);
cnt++; // 新线程修改 cnt
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, Routine, (void*)"Thread-1");
while(true)
{
printf("main thread cnt: %d\n", cnt);
printf("main thread cnt: %p\n", &cnt);
sleep(1);
}
pthread_join(tid, nullptr);
return 0;
}
如上代码,在全局变量前使用了__thread,它是线程局部存储(TLS,Thread Local Storage) 关键字,用于修饰全局或静态变量,使得每个线程拥有该变量的独立副本,互不干扰 。
如果没有这个关键字修饰的话,我们之前的代码演示过,主线程和新线程获取到的cnt是相同的,使用它修饰之后,我们应该看到它们是不同的。
编译运行:

如上图,这两个变量的地址都不相同。
线程局部存储: 只能用来局部存储内置类型,常见的是整形。它可以让不同的线程用同样的变量名,访问不同的内存块,各自访问各自的局部存储!
六、C++ 多线程
C++中也是有多线程的,线程管理在<thread>头文件中。
简单使用代码:
cpp
#include <iostream>
#include <thread>
#include <unistd.h>
void Routine(int cnt)
{
while(true)
{
std::cout << "new thread, cnt: " << cnt << std::endl;
cnt++;
sleep(1);
}
}
int main()
{
std::thread t(Routine, 10); // 创建线程
while(true)
{
std::cout << "main thread" << std::endl;
sleep(1);
}
t.join(); // 等待线程
return 0;
}
使用g++ -o TestThread TestThread.cc -std=c++11编译:

虽然,编译通过了,但是运行时发生了报错,这是因为当前在Linux平台上跑,但编译时没有链接 pthread 库。
再次编译运行:

如上,链接上pthread库就可以运行了。
所以C++多线程的本质:在Linux系统中本质是C++多线程操作时对pthread库的封装!而在Windows下,它也对Windows上的相关的库进行了封装!
因此C++才有良好的跨平台性!

所以为什么C++的新特性的支持通常很慢,一年为单位呢?因为从技术角度讲,它要把所有平台上的对应的功能都封装一遍!
所有语言都追求跨平台性是因为它们为了吸引更多的用户,这样,这门语言的生命力才会更强!
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~