还记得我们之前学的进程嘛 ?
地址空间是进程访问的资源窗口,下图当中的绿色部分就是线程
线程:在进程内部运行,是CPU调度的基本单位。 进程:承担分配系统资源的基本实体。 我们以前讲的进程内部都是只有一个执行流的进程。
CPU看到的: 执行流 <= 进程 注意:Linux中的执行流叫:轻量级进程
补充知识(LWP)
实现方式(理解):
🔥 在 Linux 统中,线程的实现并不是以传统意义上的“独立的线程”来处理的。Linux 内核并没有将线程作为独立的内核对象来管理,而是通过使用 LWP 来模拟实现的。具体来说,Linux 中的线程模型是基于 线程与进程共享同一内核资源,也就是说,线程实际上是进程的不同执行流。线程的调度和管理是通过 LWP 来实现的。
关键点:
那么我们现在该如何理解这句话:
❤️🔥 注意: Linux 中没有真正意义上的线程,Linux 的线程概念是用 LWP 进行模拟实现的!!!
clone()
)系统调用实现的,而 clone()
会创建一个新的进程(LWP),但这个新进程会共享父进程的资源。因此,Linux 的线程是依赖于进程模型的,它们共享进程的地址空间,但又具有独立的执行路径和栈💦 总而言之:Linux 中的线程的确是通过 LWP(轻量级进程)模拟实现的。每个线程在内核中都是一个 LWP,线程共享进程的资源,但内核会为每个线程分配独立的执行上下文(如栈、寄存器等)
#include <pthread.h>
功能:创建⼀个新的线程
原型:
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;失败返回错误码
错误检查:
看了线程创建,我们来用代码理解一下:LWP 的真实调度
#include <iostream>
#include <unistd.h>
#include <pthread.h>
// 新线程
void *run(void *args)
{
while(true)
{
std::cout << "new thread, pid: " << getpid() << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
std::cout << "我是一个进程: " << getpid() << std::endl;
pthread_t tid;
pthread_create(&tid, nullptr, run, (void*)"thread-1");
// 主线程
while(true)
{
std::cout << "main thread, pid: "<< getpid() << std::endl;
sleep(1);
}
return 0;
}
注意:这里直接编译,会报错,说直接创建线程是未定义的行为
在前面的运行结果中:
在这里如果我们要查看轻量级进程 ID,需要用到 ps -aL ,而不是 ps -ajx 的那种方式。
这里我们把 pid 和 lwp 都相等的执行流叫做主线程
理解:LWP与pthread_create创建的线程之间的关系
注意:我们这里指的是用户态线程
用户态线程和轻量级进程区别
特性 | 用户态线程 (User-Level Threads, ULT) | 轻量级进程 (LWP) |
---|---|---|
管理方式 | 完全由用户态的线程库(如 Pthreads)管理 | 由操作系统内核管理 |
调度方式 | 在用户态进行调度,内核不参与 | 由操作系统内核进行调度 |
上下文切换 | 在用户态完成,不涉及内核上下文切换 | 内核进行上下文切换,可能涉及内核和用户之间的切换 |
并发性 | 只能在单核上执行,因为内核无法并行调度多个线程 | 支持多核并行,因为 LWP 可以映射为多个内核线程 |
阻塞 | 一个线程阻塞时,整个进程会被阻塞 | 只有当前的 LWP 会阻塞,其他 LWP 可以继续执行 |
线程数 | 线程数较大,线程切换成本低 | 线程数相对较小,但可以在多个核上并行运行 |
资源共享 | 线程共享进程的资源,但没有独立的内核线程调度信息 | 线程共享进程的资源,但每个 LWP 通常对应一个内核线程,或多个 LWP 共享一个内核线程 |
代码演示如下:
我们把 线程 ID 以十六进制的形式打印出来
#include <iostream>
#include <unistd.h>
#include <pthread.h>
// 把线程id转化为 16 进制
std::string toHex(pthread_t tid){
char buffer[64];
snprintf(buffer, sizeof(buffer), "0x%lx", tid);
return buffer;
}
void *routine(void *args){
std::string name = static_cast<const char*>(args);
while(true){
std::cout << "我是新线程, 我的名字是" << name << ", my tid is : " << toHex(pthread_self()) << std::endl;
sleep(1);
}
return 0;
}
int main(){
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
printf("new thread id: 0x0%lx\n", tid);
while(true){
std::cout << "我是 main线程..." << std::endl;
sleep(1);
}
return 0;
}
注意:
1)多线程输出交错:
2)线程的调度和切换:
a. 性能损失
b. 健壮性降低
c. 缺乏访问控制
d. 编程难度提高
代码如下:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
// 把地址转化为 16 进制
std::string toHex(pthread_t tid){
char buffer[64];
snprintf(buffer, sizeof(buffer), "0x%lx", tid);
return buffer;
}
// 多个线程执行一个函数:routine 被重入了
// 一切皆文件:向显示器打印就是向文件写入,所以显示器文件就相当于被线程共享的公共资源
void *routine1(void *args){
std::string name = static_cast<const char*>(args);
while(true){
// 3. 不加保护的情况下,显示器文件就是共享资源
std::cout << "我是新线程, 我的名字是" << name << ", my tid is : " << toHex(pthread_self()) << std::endl;
sleep(1);
}
return 0;
}
void *routine2 (void *args){
std::string name = static_cast<const char*>(args);
while(true){
// 3. 不加保护的情况下,显示器文件就是共享资源
std::cout << "我是新线程, 我的名字是" << name << ", my tid is : " << toHex(pthread_self()) << std::endl;
sleep(1);
// 6. 线程一旦出现异常,可能会导致其他线程全部崩溃
// 6.1 异常的本质: 信号
int *p = nullptr;
*p = 100;
}
return 0;
}
int main(){
// 1. 新线程 和 main 线程谁先运行 -> 不确定
// 2. 线程创建出来,要对进程的时间片进行瓜分
pthread_t tid1;
pthread_create(&tid1, nullptr, routine1, (void*)"thread-1");
pthread_t tid2;
pthread_create(&tid2, nullptr, routine2, (void*)"thread-2");
printf("new thread id: 0x0%lx\n", tid1);
printf("new thread id: 0x0%lx\n", tid2);
while(true){
std::cout << "我是 main线程..." << std::endl;
sleep(1);
}
return 0;
}
运行结果如下:
如果新线程异常了,
虽然每个线程在进程虚拟地址空间中会分配拥有相对独立的栈空间,而并不是共享栈空间,这样会导致运行时栈混乱
同一地址空间,Text Segment、Data Segment都是共享的。
如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
进程与线程的关系如下图:
如何看待之前学习的单进程?具有一个线程执行流的进程!
#include <iostream>
#include <unistd.h>
#include <thread>
// 把地址转化为 16 进制
std::string toHex(pthread_t tid){
char buffer[64];
snprintf(buffer, sizeof(buffer), "0x%lx", tid);
return buffer;
}
void *routine(void *args){
std::string name = static_cast<const char*>(args);
while(true){
std::cout << "我是新线程, 我的名字是" << name << ", my tid is : " << toHex(pthread_self()) << std::endl;
sleep(1);
}
return 0;
}
int main(){
// 1. 新线程 和 main 线程谁先运行 -> 不确定
// 2. 线程创建出来,要对进程的时间片进行瓜分
// 3. 多个线程
pthread_t tid1;
pthread_create(&tid1, nullptr, routine, (void*)"thread-1");
printf("new thread id: 0x0%lx\n", tid1);
pthread_t tid2;
pthread_create(&tid2, nullptr, routine, (void*)"thread-2");
printf("new thread id: 0x0%lx\n", tid2);
pthread_t tid3;
pthread_create(&tid3, nullptr, routine, (void*)"thread-3");
printf("new thread id: 0x0%lx\n", tid3);
while(true){
std::cout << "我是 main线程..." << std::endl;
sleep(1);
}
return 0;
}
// 输出结果如下:
island@VM-8-10-ubuntu:~/code$ ./code
new thread id: 0x07ff7a62ea700
new thread id: 0x07ff7a5ae9700
new thread id: 0x07ff7a52e8700
我是 main线程...
我是新线程, 我的名字是thread-1, my tid is : 0x7ff7a62ea700
我是新线程, 我的名字是thread-3, my tid is : 0x7ff7a52e8700
我是新线程, 我的名字是thread-2, my tid is : 0x7ff7a5ae9700
..........
结论:
结论:全局变量在线程内都是共享的
刚刚上面演示了 一个全局变量被多线程共享,那么如果我们想让多个线程各自私有这一个变量,该怎么做呢? --> g++有一个编译选项 __thread
#include <iostream>
#include <unistd.h>
#include <pthread.h>
// 线程局部存储
// 编译型关键字,给每个线程来一份
// 虽然用的是同一份值和变量名,但是编译的时候把它地址编程不同
// 此时就叫做线程局部存储
// 注意:__thread 只能修饰内置类型
__thread int gval = 100; // 此时主线程和新线程看到的地址不同
// 把地址转化为 16 进制
std::string toHex(pthread_t tid){
char buffer[64];
snprintf(buffer, sizeof(buffer), "0x%lx", tid);
return buffer;
}
void *start(void *args)
{
std::string name = static_cast<const char*>(args);
sleep(1);
while(true){
printf("I am a new thread, name: %s, gval: %d, &gval: %p\n", name.c_str(), gval, &gval);
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start, (void*)"thread-1");
std::cout << "I am a new thread, name: main " << toHex(pthread_self())
<< ", New thread id: " << toHex(tid) << std::endl;
while(true) {
printf("main thread, gval: %d, &gval: %p\n", gval, &gval);
gval += 10;
sleep(1);
}
pthread_join(tid, nullptr);
return 0;
}
运行结果如下:
在多线程中什么都是共享的,那么这个有什么用呢?
#include <iostream>
#include <unistd.h>
#include <pthread.h>
int *addr = nullptr;
void *start1(void *args)
{
std::string name = static_cast<const char*>(args);
int a = 100;
addr = &a;
while(true){
std::cout << name << " local val a: " << a << std::endl;
sleep(1);
}
}
void *start2(void *args)
{
std::string name = static_cast<const char*>(args);
while(true){
if(addr != nullptr)
std::cout << name << " mod val a: " << (*addr)++ << std::endl;
sleep(1);
}
}
int main()
{
// 不同线程通过地址可以访问其他线程的栈
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, start1, (void*)"thread-1");
pthread_create(&tid2, nullptr, start2, (void*)"thread-2");
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}
// 输出
thread-1 local val a: 100
thread-2 mod val a: 100
thread-2 mod val a: 101
thread-1 local val a: 102
thread-1 local val a: 102
thread-2 mod val a: 102
thread-1 local val a: 103
thread-2 mod val a: 103
为什么可以访问另一个线程的栈?
注意:通过线程间共享局部栈变量的地址是非常危险的,属于未定义行为。线程 1 的栈空间在线程 1 结束时会被清理,而线程 2 持有的 addr 可能指向已被释放的内存。这样做可能会导致程序崩溃或访问无效内存
🔥 前面已经简单介绍了pthread_create的使用。在创建完成后,主线程会继续向下执行代码,新线程会去执行参数3所指向的函数。此时执行流就一分为二了
这里我们再来一个例子,用 线程传递参数 --> 结构体,来验证一个结论--> 线程传参问题
#include <iostream>
#include <unistd.h>
#include <pthread.h>
class ThreadData
{
public:
ThreadData(const std::string &name, int a, int b): _name(name), _a(a), _b(b)
{}
int Excute(){return _a + _b;}
std::string Name(){return _name;}
~ThreadData()
{}
private:
std::string _name;
int _a, _b;
};
void *routine(void *args){
ThreadData *td = static_cast<ThreadData *> (args);
while(true){
std::cout << "我是新线程, 我的名字是" << td->Name() << std::endl;
std::cout << "task result is : " << td->Excute() << std::endl;
sleep(1);
// break; // 结束当前线程死循环
}
return 0;
}
int main()
{
pthread_t tid;
ThreadData *td = new ThreadData("thread-1", 10, 20);
pthread_create(&tid, nullptr, routine, td);
printf("new thread id: 0x0%lx\n", tid);
while(true){
std::cout << "我是 main线程..." << std::endl;
sleep(1);
}
return 0;
}
结论:线程函数传参,可以传任意类型,一定要记住还可以传类对象的地址。 有了这个,就意味着可以给线程传递多个参数,甚至方法了。
对于 pthread_create 传参的问题,有两种方法,如下:
因此我们更推荐之前代码中的做法:
线程创建之后,也是需要被等待和回收的
为什么需要线程等待?
功能:等待线程结束
原型:
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
value_ptr:它指向⼀个指针,后者指向线程的返回值
参数2的类型是void**,用来接收新线程函数的返回值,因为新线程函数的返回值类型是void*。
未来要拿到新线程的返回值void*,放到void* retval中时,这里的参数就得传&retval。
返回值:成功返回0;失败返回错误码
💢 调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。thread 线程以不同的方法终止,通过 pthread join 得到的终止状态是不同的,总结如下:(这个大家可以在先看了进程终止之后再看这里)
代码如下:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
void *routine(void *args){
int cnt = 3;
while(cnt){
std::cout << "new threa run ..., cnt: " << cnt-- << std::endl;
sleep(1);
}
return 0;
}
int main(){
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
std::cout << "main thread join begin..." << std::endl;
int n =pthread_join(tid, nullptr);
while(n == 0){
std::cout << "main thread join success..." << std::endl;
sleep(1);
}
return 0;
}
// 输出:
main thread join begin...
new threa run ..., cnt: 3
new threa run ..., cnt: 2
new threa run ..., cnt: 1
main thread join success...
main thread join success...
#include <iostream>
#include <unistd.h>
#include <pthread.h>
void *routine(void *args){
int cnt = 3;
while(cnt){
std::cout << "new threa run ..., cnt: " << cnt-- << std::endl;
sleep(1);
}
return (void*)1314;
}
int main(){
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
std::cout << "main thread join begin..." << std::endl;
void *ret = nullptr; // 开辟了空间的
int n = pthread_join(tid, &ret); // 传递该空间地址
std::cout << "join success! ret: " << (long long int) ret <<std::endl; // 等待新线程退出后,结束等待
return 0;
}
返回值还可以是类对象的地址,主线程接收时用对应类类型对象接收即可
注意:在这里只考虑正确的返回值,不考虑异常,因为异常时整个程序就挂掉了。
#include <iostream>
#include <unistd.h>
#include <pthread.h>
class ThreadData
{
public:
ThreadData()
{}
void Init(const std::string &name, int a, int b){
_name = name;
_a = a;
_b = b;
}
void Excute(){_result = _a + _b;}
int Result(){return _result;}
std::string Name(){return _name;}
void SetId(pthread_t tid) {_tid = tid;}
pthread_t Id() {return _tid;}
int A() {return _a;}
int B() {return _b;}
~ThreadData()
{}
private:
std::string _name;
int _a, _b;
int _result; // 返回结果
pthread_t _tid;
};
int gval = 100;
std::string toHex(pthread_t tid){
char buffer[64];
snprintf(buffer, sizeof(buffer), "0x%lx", tid);
return buffer;
}
void *routine(void *args){
ThreadData *td = static_cast<ThreadData *> (args);
while(true){
std::cout << "我是新线程, 我的名字是" << td->Name() << ", my tid is : " << toHex(pthread_self()) << ", 全局变量(会修改): " << gval << std::endl;
gval++;
td->Excute();
sleep(1);
break; // 结束当前线程死循环
}
return td;
}
// 现在有一批数据,每一个数据的结果都让线程进行运算并且进行汇总
#define NUM 10
int main()
{
ThreadData td[NUM];
// 1. 准备好我们要价格处理的数据
for(int i = 0; i < NUM; i++){
char id[64];
snprintf(id, sizeof(id), "thread-%d", i);
td[i].Init(id, i * 10, i * 20);
}
// 2. 创建多线程
for(int i = 0; i < NUM; i++){
pthread_t id;
pthread_create(&id, nullptr, routine, &td[i]);
td[i].SetId(id);
}
// 3. 等待多个线程
for(int i = 0; i < NUM; i++)
{
pthread_join(td[i].Id(), nullptr);
}
// 4. 汇总处理结果
for(int i = 0; i < NUM; i++)
{
printf("td[%d]: %d + %d = %d[%ld]\n", i, td[i].A(), td[i].B(), td[i].Result(), td[i].Id());
}
return 0;
}
运行结果如下:
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
pthread exit函数
功能:线程终⽌
原型:
void pthread_exit(void *value_ptr);
参数:
value_ptr:value_ptr不要指向⼀个局部变量。
返回值:
⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)
pthread cancel函数
功能:取消⼀个执⾏中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:
thread:线程ID
返回值:成功返回0;失败返回错误码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
void *start(void *args)
{
std::string name = static_cast<const char*>(args);
while(true){
std::cout << "I am a new thread" << std::endl;
sleep(1);
//break;
}
// return 0; // 10. 新线程 return 表示该线程退出
// exit(1); // 任何地方内部调用 exit,表示进程退出!! 尽管是在线程内部
//pthread_exit((void*)10); // 第二种退出方法,和 return 等价
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start, (void*)"thread-1");
sleep(3);
pthread_cancel(tid); // 第三种退出方式
std::cout << "取消线程: " << tid << std::endl;
sleep(3);
void *ret = nullptr;
//一个线程被取消之后,ret = -1,线程的退出码是 PTHREAD_CANCELD
int n = pthread_join(tid, &ret);
std::cout << "new thread exit code: " << (long long int)ret << ", n: " << n << std::endl;
return 0; // 10. 主线程return: 表示进程结束
}
运行结果如下:
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对⽬标线程进⾏分离,也可以是线程自己分离:
int pthread_detach(pthread_self());
#include <iostream>
#include <unistd.h>
#include <pthread.h>
void *start(void *args)
{
pthread_detach(pthread_self()); // 线程自己把自己分离
std::string name = static_cast<const char*>(args);
while(true){
std::cout << "I am a new thread" << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start, (void*)"thread-1");
// 除了可以让新线程自己分离,也可以由主线程进行分离
//pthread_detach(tid); // 主线程对指定线程做分离
sleep(3);
void *ret = nullptr;
int n = pthread_join(tid, &ret);
std::cout << "new thread exit code: " << (long long int)ret << ", n: " << n << std::endl;
return 0;
}
运行结果如下:
原因:
注意:在多执行流情况下,主执行流是最后退出的
pthread_t pthread_self(void);
pthread_t 到底是什么类型呢?取决于实现。对于Linux⽬前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
这里我想要说一下的就是关于 头文件 <thread> 和 头文件 <pthread.h>
在 C++ 中,<thread> 和 pthread.h 都用于线程编程,但它们分别属于不同的库和标准,并有不同的特性。下面我将详细介绍这两个头文件以及它们的区别。
<thread> 是 C++11 标准引入的头文件,提供了对多线程编程的标准支持。使用这个头文件,C++ 程序可以方便地创建和管理线程。它提供了一个高级的线程抽象,封装了底层的线程管理,使得多线程编程变得更简洁。
主要特点:
示例代码:
#include <iostream>
#include <thread>
void print_hello() {
std::cout << "Hello from the thread!" << std::endl;
}
int main() {
// 创建一个新线程
std::thread t(print_hello);
// 等待线程执行完毕
t.join(); // join() 会阻塞当前线程,直到新线程完成
std::cout << "Main thread finished." << std::endl;
return 0;
}
// 输出:
Hello from the thread!
Main thread finished.
主要功能:
pthread.h 是 POSIX 标准中定义的线程库,通常用于类 Unix 操作系统(如 Linux 和 macOS)。这个库提供了更底层的线程控制,使用起来相对复杂,需要更多的手动管理,但也提供了更灵活的功能。
主要特点:
示例代码:
#include <iostream>
#include <pthread.h>
void* print_hello(void* arg) {
std::cout << "Hello from the thread!" << std::endl;
return nullptr;
}
int main() {
pthread_t thread;
// 创建一个新线程
pthread_create(&thread, nullptr, print_hello, nullptr);
// 等待线程执行完毕
pthread_join(thread, nullptr); // 阻塞主线程,直到新线程结束
std::cout << "Main thread finished." << std::endl;
return 0;
}
输出:
Hello from the thread!
Main thread finished.
主要功能:
特性 | <thread> 头文件 (C++11) | <pthread.h> 头文件 (POSIX) |
---|---|---|
标准 | C++11 标准 | POSIX 标准(主要在类 Unix 系统上) |
接口层次 | 高级抽象,面向对象的接口 | 低级抽象,面向过程的接口 |
跨平台支持 | 支持 C++11 标准的所有平台 | 主要支持类 Unix 操作系统 |
线程创建 | std::thread | pthread_create() |
线程同步 | join(), detach() | pthread_join(), pthread_detach() |
同步工具 | 通过 C++ 标准库的 <mutex> 和 <condition_variable> 提供 | 提供互斥锁(pthread_mutex_t)和条件变量(pthread_cond_t)等 |
异常处理 | 支持线程抛出异常,并能传播至主线程 | 需要手动管理异常处理 |
使用难度 | 更简洁,容易理解 | 更复杂,更多控制,适合低级编程 |
使用 <thread>:
使用 pthread.h:
总的来说
那么这里我们就把线程的概念以及控制就讲完啦,后面我们就要开始线程互斥与等待啦,敬请期待!!!
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!