
文章目录
线程(Thread)
1. 什么是线程?
线程是进程中的一个执行单元,它是 CPU 调度的基本单位 。线程依赖于进程存在,一个进程可以包含多个线程,这些线程可以并发执行,提高程序的运行效率。
进程是承担系统分配系统资源的实体
线程是操作系统调度的基本单位
用一张图简要说明一下什么是线程:
首先我们要知道,在Linux中是没有实际的线程的,线程是被模拟出来的,Linux实际上使用LWP模拟的线程。
LWP(Light Weight Process,轻量级进程)是 Linux 线程实现的一种机制,它与传统进程共享大部分资源,但仍有自己的调度信息。
创建线程

pthread_create是用于创建线程的函数,这个函数不是系统调用,因为Linux实际上是没有实体的线程,这个创建线程的函数是在pthread.h中封装的函数。
这个函数的第一个参数是pthread_t*类型的,pthread_t表示线程id,第二个参数表示线程属性,要是我们不观星的话,就直接传递nullptr,第三个参数是线程执行函数,就是函数指针,新线程就是从这个函数的入口开始执行的,第四个参数是传递给执行新线程的函数的参数。
代码展示:
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *routine(void* args)
{
string name = static_cast<const char*>(args);
while(true)
{
cout << "I am new thread,the name is:" << name << endl;
sleep(1);
}
}
int main()
{
pthread_t id;
pthread_create(&id,nullptr,routine,(void*)"thread-1");
while(true)
{
cout << "I am main thread" << endl;
sleep(1);
}
return 0;
}
如果真的创建了一个新线程,那么routine函数也会跑起来,这里有两个执行流。
可以看见确实有两个执行流。
创建线程之后,我们来讨论一下这个id是什么,没错就是线程id,我们打印一下:
可以看到转化为16进制之后打印出来的是这个,如何用函数获得线程id呢?在库中有一个函数可以获取线程的id:
这个函数不用传任何参数,可以获取本线程的id,我们来测试一下这个函数的正确性:
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
//转化为十六进制
string toHex(pthread_t tid)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"0x%lx",tid);
return buffer;
}
void *routine(void* args)
{
string name = static_cast<const char*>(args);
while(true)
{
cout << "I am new thread,the name is:" << name <<",this thread's id is:" << toHex(pthread_self()) <<endl;
sleep(1);
}
}
int main()
{
pthread_t id;
pthread_create(&id,nullptr,routine,(void*)"thread-1");
cout<<toHex(id)<<endl;
while(true)
{
cout << "I am main thread" << endl;
sleep(1);
}
return 0;
}

可以看到结果是一致的。
我们现在已经创建了一个新的线程,意思就是我们现在有两个线程,我们来查一下:
不加任何选项查线程只能查出进程。
cpp
ps -aL

可以看见确实有两个线程,两个线程的pid是相同的,那哪一个是主线程,哪一个是新线程呢?pid和lwp相同的是主线程,pid和lwp不同的是新线程。
多线程中的重入问题
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
//转化为十六进制
string toHex(pthread_t tid)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"0x%lx",tid);
return buffer;
}
void *routine(void* args)
{
string name = static_cast<const char*>(args);
while(true)
{
cout << "I am new thread,the name is:" << name <<",this thread's id is:" << toHex(pthread_self()) <<endl;
sleep(1);
}
}
int main()
{
pthread_t id1;
pthread_create(&id1,nullptr,routine,(void*)"thread-1");
pthread_t id2;
pthread_create(&id2,nullptr,routine,(void*)"thread-2");
pthread_t id3;
pthread_create(&id3,nullptr,routine,(void*)"thread-3");
pthread_t id4;
pthread_create(&id4,nullptr,routine,(void*)"thread-4");
while(true)
{
cout << "I am main thread" << endl;
sleep(1);
}
return 0;
}
我们可以同时创建多个线程:
我们运行之后,可以看见,有时候显示器屏幕上会出现混乱的现象,这是因为当我们创建多个线程的时候,这多个线程同时访问routine函数,routine函数就被重入了,因为routine函数中有cout这个io函数,这个函数的本质其实是在访问显示器文件,在向显示器文件上写入,所以这里显示器文件是公共资源,被多个线程访问,因为这里的公共资源没有加上保护,所以每个线程想怎么打印就怎么打印,所以有时候会出现混乱的现象。
线程异常
当某一个线程出现野指针或者除0等错误异常时,整个进程都会退出,这是因为,当某一个线程触发异常时,在CPU当中会触发中断,触发中断后,会拿着中断去找处理方法,找到之后,就会处理信号,因为信号的处理单位时进程,所以处理信号的时候会直接改写每个线程的信号表,将每个线程直接杀死。
线程等待
不光是进程需要等待,线程也是需要等待的,线程和进程一样,默认等待方式时阻塞等待。
线程等待函数:
第一个参数是线程的id,第二个参数是二级指针,如果我们不关心退出结果,那么我们可以直接将第二个参数设置为nullptr。
cpp
void *routine(void* args)
{
string name = static_cast<const char*>(args);
while(true)
{
cout << "I am new thread,the name is:" << name <<",this thread's id is:" << toHex(pthread_self()) <<endl;
sleep(1);
break;
}
}
int main()
{
pthread_t id1;
pthread_create(&id1,nullptr,routine,(void*)"thread-1");
pthread_join(id1,nullptr);
return 0;
}
我们让新线程跑一秒钟,然后跑完直接退出,看看会是什么结果。
可以看见跑完一秒钟后直接就退出了。
假如我们关心退出结果呢?

我们打印一下退出结果,可以看见,退出结果就是新线程的返回值。
这里我们可以来通过返回值验证一下堆空间是线程之间共享的。
cpp
void *routine(void* args)
{
string name = static_cast<const char*>(args);
while(true)
{
cout << "I am new thread,the name is:" << name <<",this thread's id is:" << toHex(pthread_self()) <<endl;
sleep(1);
break;
}
int *p = new int(10);
return (void*)p;
}
int main()
{
pthread_t id1;
pthread_create(&id1,nullptr,routine,(void*)"thread-1");
void* ret = nullptr;
pthread_join(id1,&ret);
cout<<*(int*)ret<<endl;
return 0;
}

这类我们在新线程中new一个空间,线程退出之后,主线程是可以访问到new出的地址的,new是在堆空间中开辟的,所以这里可以看出堆空间是共享的,其实如果我们想的话,栈空间也是可以共享的,只需要在新线程中创建一个int变量,然后创建一个全局变量,int*的,然后然后在新线程中,把那个变量的地址给全局变量,其实在主线程中,我们是可以修改这个变量的。所以也验证了堆空间,虽然没办法共享,但是也是可以想办法看到的。
总结
在本篇文章中,我们深入探讨了线程的基本概念、创建方式、多线程中的重入问题、线程异常处理以及线程等待机制。通过这些内容,我们可以更好地理解线程的工作方式,并在实际开发中合理运用多线程技术,提高程序的并发性能。
多线程编程虽然能够提升程序的执行效率,但也伴随着诸多挑战,如数据竞争、死锁、线程安全等问题。因此,在使用多线程时,我们需要充分考虑同步与互斥机制,合理设计程序结构,以确保线程安全性和稳定性。
希望本篇文章能帮助你更好地理解多线程的基础知识,并在实践中灵活应用。如果你有更多问题或想深入学习,可以继续探索线程池、锁优化、异步编程等更高级的内容。