✨个人主页:熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】
目录
[4、进程 VS 线程](#4、进程 VS 线程)
1、线程创建
线程能看到进程的大部分资源,下面做一个对全局变量修改的测试验证!!!
代码演示
int gval = 100;
void* threadStart(void* args)
{
// 新线程
while(true)
{
std::cout << "new thread running..." << ",pid: " << getpid()
<< ",gval: " << gval << ",&gval: " << &gval << std::endl;
sleep(1);
}
}
// 线程访问全局变量
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadStart,(void*)"thread-new");
// 主线程
while(true)
{
std::cout << "main thread running..." << ",pid: " << getpid()
<< ",gval: " << gval << ",&gval: " << &gval << std::endl;
gval++; // 主线程修改全局变量
sleep(1);
}
return 0;
}
运行结果
2、线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃 ,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
代码演示
// 单个线程崩溃,会导致进程崩溃
int gval = 100;
void *threadStart(void *args)
{
// 新线程
while (true)
{
sleep(1);
int x = rand() % 5; // 生成0-4的随机数
std::cout << "new thread running..." << ",pid: " << getpid()
<< ",gval: " << gval << ",&gval: " << &gval << std::endl;
// 随机数等于0则让线程崩溃
if (x == 0)
{
int *p = nullptr; // 空指针解引用问题
*p = 100;
}
}
}
// 线程访问全局变量
int main()
{
srand(time(nullptr));
// 创建3个线程
pthread_t tid1;
pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new1");
pthread_t tid2;
pthread_create(&tid2, nullptr, threadStart, (void *)"thread-new2");
pthread_t tid3;
pthread_create(&tid3, nullptr, threadStart, (void *)"thread-new3");
// 主线程
while (true)
{
std::cout << "main thread running..." << ",pid: " << getpid()
<< ",gval: " << gval << ",&gval: " << &gval << std::endl;
gval++; // 主线程修改全局变量
sleep(1);
}
return 0;
}
运行结果
3、线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
4、进程 VS 线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器(保存硬件上下文数据)
- 栈(程序在运行的时候,会形成各种临时变量,临时变量被每个线程保存在自己的栈区)
- errno
- 信号屏蔽字
- 调度优先级
进程的多个线程共享 同一地址空间 ,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
进程和线程的关系如下图:
如何看待之前学习的单进程?
具有一个线程执行流的进程 。
线程调度成本为什么比进程更低?(面试题)
一、进程与线程的基本概念
- 进程 :进程是资源分配的最小单位,每个进程都有自己独立的地址空间,系统需要为进程分配地址空间并建立数据表来维护其代码段、堆栈段和数据段。这种操作相对复杂且开销较大。
- 线程 :线程是程序执行的最小单位(资源调度的最小单位),它是进程的一部分,共享进程所拥有的资源。因此,线程切换时无需像进程切换那样重新分配地址空间和维护数据表,从而减少了开销。
二、上下文切换的开销
- CPU上下文切换:无论是进程调度还是线程调度,都需要进行CPU上下文切换。这部分开销在两者中是相似的。
- CPU Cache/TLB命中率 :线程切换时,由于多个线程共享进程的地址空间,因此CPU Cache(高速缓存)和TLB(转换后备缓冲器)中的内容在切换后仍然有效,命中率较高。而进程切换时,由于地址空间的变化,原有的Cache和TLB内容可能不再适用,导致命中率下降,触发更多的缺页中断,从而增加了开销。
三、资源共享与通信
- 资源共享 :线程共享进程的资源,包括地址空间、全局变量、静态变量等。这使得线程之间的通信更加便捷,无需像进程间通信那样通过IPC(进程间通信)方式进行,从而减少了通信开销。
- 通信开销 :进程间通信需要借助额外的机制(如管道、信号、共享内存等),这些机制的实现和维护都会增加开销。而线程间通信则可以直接通过共享内存进行,无需额外的通信机制。
四、创建与销毁的开销
- 创建开销 :由于进程需要分配独立的地址空间和维护数据表,因此创建进程的开销相对较大。而线程则共享进程的地址空间,无需进行这些操作,因此创建线程的开销较小。
- 销毁开销 :同样地,由于进程拥有独立的资源,因此在销毁时需要释放这些资源,开销较大。而线程则无需释放独立的资源,销毁开销相对较小。
5、线程控制
线程控制:创建,终止,等待,分离!
POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"打头的
- 要使用这些函数库,要通过引入头文<pthread.h>
- 链接这些线程函数库时要使用编译器命令的"-lpthread"选项
5.1、创建和等待线程
pthread_join()
pthread_join - 等待指定的线程终止
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数:
pthread_t thread
:这是你想要等待的线程的标识符。线程标识符是在创建线程时通过pthread_create
函数返回的。void **retval
:这是一个指向指针的指针(二级指针),用于接收被等待线程的返回值。如果你不需要获取线程的返回值,可以将这个参数设置为 nullptr 。被等待线程的返回值应该是一个void*
类型的指针,在调用pthread_exit
或从线程的启动函数返回时设置。
返回值:
- 成功时 ,
pthread_join
返回0
。 - 失败时,返回一个错误码 。常见的错误码包括:
ESRCH
:指定的线程不存在。EINVAL
:线程不是可连接的(即,线程不是可加入的,可能因为它已经终止了,或者它是以分离状态创建的)。EDEADLK
:检测到死锁(在尝试加入一个已经由调用线程加入的线程时可能发生)。- 其他可能的错误码,具体取决于系统实现。
代码演示
新线程执行函数
void *threadRun(void *args)
{
int cnt = 10;
while(cnt)
{
// 每隔一秒打印一次
std::cout << "new thread run...,cnt: " << cnt-- << std::endl;
sleep(1);
}
return nullptr;
}
主函数
int main()
{
pthread_t tid;
// 创建新线程
int n = pthread_create(&tid, nullptr, threadRun, (void *)"thread 1");
if (n != 0) // 后面暂时不关心
{
std::cerr << "create thread errno " << std::endl;
return 1;
}
std::cout << "main thread join begin..." << std::endl;
// 等待新线程终止
n = pthread_join(tid,nullptr);
if(n == 0)
{
std::cout << "main thread wait success " << std::endl;
}
return 0;
}
运行结果
问题1 : main 和 new 线程谁先运行?
不确定
问题2 : 我们期望谁最后退出?
main thread最后退出,类似与父进程最后退出,回收子进程 , 你如何保证呢?
- join来保证。 不join呢?
- 主线程活着,新线程退出会造成类似僵尸问题
问题3 :tid是什么样子的?是什么呢?
tid通过10进制打印是一个很大的值,tid实际上是一个虚拟地址,可以通过16进制进行打印。
打印函数
// 10进制打印tid
void PrintToDec(pthread_t &tid)
{
std::cout << "tid: " << tid << std::endl;
}
// 16进制打印tid
std::string PrintToHex(pthread_t &tid)
{
char buffer[128];
snprintf(buffer,sizeof(buffer),"0x%lx",tid);
return buffer;
}
主函数
int main()
{
pthread_t tid;
// 创建新线程
int n = pthread_create(&tid, nullptr, threadRun, (void *)"thread 1");
// 问题3 : tid是什么样子的?是什么呢?虚拟地址! 为什么?
PrintToDec(tid); // 按照10进制方式打印
std::string tid_str = PrintToHex(tid); // 按照16进制方式打印
std::cout << "tid: " << tid_str << std::endl;
std::cout << "main thread join begin..." << std::endl;
// 等待新线程终止
n = pthread_join(tid,nullptr);
if(n == 0)
{
std::cout << "main thread wait success " << std::endl;
}
return 0;
}
运行结果
问题4 : 全面看待线程函数传参?
我们可以传递任意类型,但你一定要能想得起来,也能传递类对象地址!!
方式一:传字符串常量
代码演示
void *threadRun(void *args)
{
std::string name = (const char*)args;
int cnt = 10;
while(cnt)
{
// 每隔一秒打印一次
std::cout << name << " run...,cnt: " << cnt-- << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
// 创建新线程
int n = pthread_create(&tid, nullptr, threadRun, (void *)"thread 1");
std::string tid_str = PrintToHex(tid); // 按照16进制方式打印出来
std::cout << "tid: " << tid_str << std::endl;
std::cout << "main thread join begin..." << std::endl;
// 等待新线程终止
n = pthread_join(tid,nullptr);
if(n == 0)
{
std::cout << "main thread wait success " << std::endl;
}
return 0;
}
运行结果
方式二:传整数
代码演示
void *threadRun(void *args)
{
int a = *(int*)args;// warning 系统为64位,指针大小为8字节,int为4字节
int cnt = 10;
while(cnt)
{
std::cout << a << " run...,cnt: " << cnt-- << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
int a = 100;
int n = pthread_create(&tid, nullptr, threadRun, (void *)&a);
std::string tid_str = PrintToHex(tid); // 按照16进制方式打印出来
std::cout << "tid: " << tid_str << std::endl;
std::cout << "main thread join begin..." << std::endl;
// 等待新线程终止
n = pthread_join(tid,nullptr);
if(n == 0)
{
std::cout << "main thread wait success " << std::endl;
}
return 0;
}
运行结果
方式二:传类对象
代码演示
class ThreadData
{
public:
std::string name;
int num;
};
void *threadRun(void *args)
{
ThreadData* td = static_cast<ThreadData*>(args); // 安全类别强转 (ThreadData*)args
int cnt = 10;
while(cnt)
{
std::cout << td->name << " run...,num is " << td->num << ",cnt: " << cnt-- << std::endl;
sleep(1);
}
return nullptr;
}
主函数
int main()
{
pthread_t tid;
ThreadData td;
td.name = "thread-1";
td.num = 1;
int n = pthread_create(&tid, nullptr, threadRun, (void*)&td); // 传递线程结构体对象
std::string tid_str = PrintToHex(tid); // 按照16进制方式打印出来
std::cout << "tid: " << tid_str << std::endl;
std::cout << "main thread join begin..." << std::endl;
// 等待新线程终止
n = pthread_join(tid,nullptr);
if(n == 0)
{
std::cout << "main thread wait success " << std::endl;
}
return 0;
}
运行结果
创建新线程访问栈上的空间不推荐 ,因为当多个新线程访问同一个结构体数据时,可能造成数据互相影响的问题,如果只读问题不大,但是如果一个线程对该数据进行修改,那么后面所有线程访问的数据都会修改!!!
// 再创建一个新线程,使用同一个局部变量,修改值两个都修改了
td.name = "thread-2";
td.num = 2;
n = pthread_create(&tid, nullptr, threadRun, (void*)&td); // 传递线程结构体对象
运行结果
推荐在堆上申请空间,一个新线程申请一个类对象,使用完毕释放空间!
void *threadRun(void *args)
{
ThreadData* td = static_cast<ThreadData*>(args); // 安全类别强转 (ThreadData*)args
int cnt = 10;
while(cnt)
{
std::cout << td->name << " run...,num is " << td->num << ",cnt: " << cnt-- << std::endl;
sleep(1);
}
std::cout << "delete td:" << td << std::endl;
delete td; // 释放空间
return nullptr;
}
int main()
{
pthread_t tid;
ThreadData* td = new ThreadData();
td->name = "thread-1";
td->num = 1;
int n = pthread_create(&tid, nullptr, threadRun, td);
std::string tid_str = PrintToHex(tid); // 按照16进制方式打印出来
std::cout << "tid: " << tid_str << std::endl;
std::cout << "main thread join begin..." << std::endl;
// 等待新线程终止
n = pthread_join(tid,nullptr);
if(n == 0)
{
std::cout << "main thread wait success " << std::endl;
}
return 0;
}
运行结果
问题5: 全面看待线程函数返回:?
新线程函数返回值
1、只考虑正确的返回,不考虑异常,因为异常了,整个进程就崩溃了,包括主线程。
新线程通过函数返回值给主线程!!!
代码演示
void *threadRun(void *args)
{
ThreadData* td = static_cast<ThreadData*>(args); // 安全类别强转 (ThreadData*)args
int cnt = 10;
while(cnt)
{
std::cout << td->name << " run...,num is " << td->num << ",cnt: " << cnt-- << std::endl;
// int* p = nullptr;
// *p = 100; // 故意野指针
sleep(1);
}
std::cout << "delete td:" << td << std::endl;
delete td; // 释放空间
return (void*)111;
}
主线程获取新线程的返回值信息!!!
int main()
{
pthread_t tid;
ThreadData* td = new ThreadData();
td->name = "thread-1";
td->num = 1;
int n = pthread_create(&tid, nullptr, threadRun, td);
std::cout << "main thread join begin..." << std::endl;
// 等待新线程终止
void* code = nullptr; // 开辟了空间的!!!
n = pthread_join(tid,&code);
if(n == 0)
{
// 主线程拿新线程的退出信息,int会有精度损失,Linux中地址8字节,int4字节
std::cout << "main thread wait success, new thread exit code: " << (uint64_t)code << std::endl;
}
return 0;
}
运行结果
新线程故意野指针!!!
运行结果
2、我们可以传递任意类型,但你一定要能想得起来,也能传递类对象地址!!
类对象
class ThreadData
{
public:
int Excute()
{
return x + y;
}
public:
std::string name;
int x;
int y;
};
class ThreadResult
{
public:
std::string Print()
{
return std::to_string(x) + "+" + std::to_string(y) + "=" + std::to_string(result);
}
public:
int x;
int y;
int result;
};
新线程函数
void *threadRun(void *args)
{
ThreadData* td = static_cast<ThreadData*>(args);
int cnt = 10;
ThreadResult* result = new ThreadResult();
while(cnt)
{
sleep(3);
std::cout << td->name << " run...,cnt: " << cnt-- << std::endl;
result->result = td->Excute();
result->x = td->x;
result->y = td->y;
break;
}
std::cout << "delete td:" << td << std::endl;
delete td; // 释放空间
return (void*)result;
}
主函数
int main()
{
pthread_t tid;
ThreadData* td = new ThreadData();
td->name = "thread-1";
td->x = 10;
td->y = 20;
int n = pthread_create(&tid, nullptr, threadRun, td);
std::cout << "main thread join begin..." << std::endl;
// 等待新线程终止
ThreadResult* result = nullptr; // 开辟了空间的!!!
n = pthread_join(tid,(void**)&result);
if(n == 0)
{
std::cout << "main thread wait success, new thread exit code: " << result->Print() << std::endl;
}
return 0;
}
运行结果
问题6 : 如何创建多线程呢?
错误示范(X)
在for循环内部创建临时变量!!!
代码演示
cpp
const int num = 10;
void *threadrun(void *args)
{
std::string name = static_cast<const char *>(args);
while (true)
{
// 打印的线程名是乱的,线程执行顺序是不确定的,
// 且因为在名字栈区for循环内部创建,每切换一个线程,名字就会被覆盖,有问题!!!
std::cout << name << " is running" << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
// 问题6 : 如何创建多线程呢?
std::vector<pthread_t> tids;
for (int i = 0; i < num; i++)
{
// 1.有线程的id
pthread_t tid;
// 2.有线程的名字
char name[128];
snprintf(name, sizeof(name), "thread-%d", i + 1);
pthread_create(&tid, nullptr, threadrun, /*线程的名字*/ name);
}
// join todo
sleep(100);
return 0;
}
运行结果
正确示范
只需让name在堆区创建即可,并修改格式化name函数
cpp
// 2.有线程的名字(正确示范)
char* name = new char[128];
snprintf(name, 128, "thread-%d", i + 1);
等待(终止)多线程
创建好新线程之后,保存每个线程的tid,遍历vector终止新线程即可!
代码演示
cpp
const int num = 10;
void *threadrun(void *args)
{
std::string name = static_cast<const char *>(args);
while (true)
{
// 打印的线程名是乱的,线程执行顺序是不确定的,
// 且因为在名字栈区for循环内部创建,每切换一个线程,名字就会被覆盖,有问题!!!
std::cout << name << " is running" << std::endl;
sleep(1);
break;
}
// return nullptr;
return args;
}
int main()
{
// 问题6 : 如何创建多线程呢?
std::vector<pthread_t> tids;
for (int i = 0; i < num; i++)
{
// 1.有线程的id
pthread_t tid;
// 2.有线程的名字(错误示范)
// char name[128];
// snprintf(name, sizeof(name), "thread-%d", i + 1);
// 2.有线程的名字(正确示范)
char* name = new char[128];
snprintf(name, 128, "thread-%d", i + 1);
pthread_create(&tid, nullptr, threadrun, /*线程的名字*/ name);
// 3.保存所有线程的id信息
tids.emplace_back(tid);
}
// join todo
for(auto tid : tids)
{
void* name = nullptr;
pthread_join(tid,&name);
// std::cout << PrintToHex(tid) << " quit" << std::endl;
std::cout << (const char*)name << " quit" << std::endl;
delete (const char*)name;
}
// sleep(100);
return 0;
}
tid方式打印
运行结果
线程名方式打印
运行结果