文章目录
- 👉知识补充👈
- 👉Linux线程概念👈
- 👉线程控制👈
-
- 线程终止
- [pthread_exit 函数](#pthread_exit 函数)
- [pthread_cancel 函数](#pthread_cancel 函数)
- [线程 ID 的深入理解](#线程 ID 的深入理解)
- 在多线程的场景下进行进程替换
- 线程等待
- 👉总结👈
👉知识补充👈
👉Linux线程概念👈
什么是线程
- 线程是在进程内部执行的,也就是说线程是在进程的地址空间内运行的,其是操作系统调度的基本单位。
- 进程等于内核数据结构加上该进程对应的代码和数据,内核数据结构可能不止一个 PCB,进程是承担分配系统资源的基本实体,将资源分配给线程!
- 那如何理解我们之前写的代码呢?其实我们之前学习的是只有一个执行流的进程,而今天学习的是具有多个执行流的进程(task_struct 是进程内部的一个执行流),所以这两者是不冲突的。
- 在运行队列中排队的都是 task_struct,CPU 只能看到 task_struct,CPU 根本不关系当前调度的是进程还是线程,只关心 task_struct。所以,CPU 调度的基本单位是"线程"。
- Linux 下的线程是轻量级进程,没有真正意义上的线程结构,没有为线程专门设计内核数据结构,而是通过 PCB 来模拟实现出线程的。
- Linux 并不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口!在用户层实现了一套多进程方案,以库的方式提供给用户进行使用,这个库就是 pthread 线程库(原生线程库)。
知道了什么是线程,我们来学习创建线程的接口,来验证一下上面的结论!
pthread_create 函数的功能是创建一个新的线程。thread 是输出型参数,返回进程的 ID;attr 设置线程的属性,attr 为 nullptr 表示使用默认属性;start_routine 是一个函数地址,即线程启动后要执行的函数;arg 是传给线程启动函数的参数。调用成功是返回 0,错误是返回错误码。
Makefile
cpp
mythread:mythread.cc
g++ $^ -o $@ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f mythread
注:使用原生线程库时,必须带上 -lpthread
,告诉编译器你要链接原生线程库,否则就会产生链接错误。
cpp
// 注:一下代码是示例代码,有些许问题
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <string>
using namespace std;
void* threadRun(void* args)
{
string name = (char*)args;
while(1)
{
cout << name << " id: " << getpid() << '\n' << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid[5];
char name[64];
for (int i = 0; i < 5; i++)
{
snprintf(name, sizeof name, "%s-%d", "thread", i);
pthread_create(tid + i, nullptr, threadRun, (void*)name);
sleep(3); // 缓解传参的bug
}
while (true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(3);
}
return 0;
}
cpp
ps -aL | head -1 && ps -aL | grep mythread | grep -v grep #查找线程
将进程 16889 杀掉时,全部执行流都会终止。因为线程用的资源都是进程给的,而杀掉进程就要回收进程的资源,那么线程终止了是理所当然的。
线程是如何看到进程内部的资源的呢?
我们知道,线程的运行依赖于进程的资源,一旦进程退出,线程也会退出。那进程的哪些资源是线程之间共享的,哪些资源又是线程独自占用的呢?
进程的大多数资源都被线程所共享:
- 文件描述符表,如果一个线程打开了一个文件,那么其他的线程也能够看到。
- 每种信号的处理方式(SIG_IGN、SIG_DFL 或者自定义的信号处理函数)
- 当前工作目录
- 用户 ID 和组 ID
- 进程地址空间的代码区、共享区
- 已初始化、未初始化数据区,也就是全局变量
- 堆区一般也是被所有线程共享的,但在使用时,认为线程申请的堆空间是线程私有的,因为只有这个线程拿到这段空间的其实地址
线程独自占用的资源:
- 线程 ID
- 一组寄存器。线程是 CPU 调度的基本单位,一个线程被调度一定会形成自己的上下文,那么这组寄存器必须是私有的,才能保证正常的调度。
- 栈。每个线程都是要通过函数来完成某种任务的,函数中会定义各种临时变量,那么线程就需要有自己私有的栈来保存这些局部变量。
- 错误码 errno、信号屏蔽字、调度优先级
线程 VS 进程
为什么线程的调度切换的成本更低呢?
线程进行切换时,进程地址空间和页表是不用换的。而进程进行切换时,需要将进程的上下文,进程地址空间、页表、PCB 等都要切换。CPU 内部是有 L1 ~ L3 的 Cache,CPU 执行指令时,会更具局部性原理将内存中的代码和数据预读到 CPU 的缓存中。如果是多线程,CPU 预读的代码和数据很大可能就会被所有的线程共享,那么进行线程切换时,下一个线程所需要的代码和数据很有可能已经被预读了,这样线程切换的成本就会更低!而进程具有独立性,进行进程切换时,CPU 的 Cache 缓存的代码和数据就会立即失效,需要将新进程的代码和数据重新加载到 Cache 中,所以进程切换的成本是更高的。
进程和线程的关系如下图:
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速 I / O 操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I / O 密集型应用,为了提高性能,将 I / O 操作重叠。线程可以同时等待不同的 I / O 操作
注:线程不是创建越多越好,因为线程切换也是有成本的,并不是不需要成本。创建线程太多了,线程切换的成本有可能就是最大的成本了。
线程的缺点
- 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。如:一个线程对全局变量修改了,另外的线程的全局变量也会跟着修改;还有就是如果主线程挂掉了,其他线程也会跟着挂掉。
- 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些操作系统函数会对整个进程造成影响。
- 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多。
线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程。进程终止,该进程内的所有线程也就随即退出。
线程用途
- 合理的使用多线程,能提高 CPU 密集型程序的执行效率。
- 合理的使用多线程,能提高 I / O 密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。
👉线程控制👈
clone 函数可以创建线程或者子进程,可以设置回调函数,子进程的栈区,还有各种属性等等。除了 clone 函数,还有一个 vfork 函数。vfork 函数创建出来的子进程是和父进程共享进程地址空间的。
cpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int globalVal = 100;
int main()
{
int id = vfork();
// int id = fork();
assert(id != -1);
if(id == 0)
{
// child process
int count = 0;
while(1)
{
cout << "child process -> globalVal: " << globalVal << endl;
sleep(1);
++count;
if(count == 5)
{
globalVal = 200;
cout << "child process change globalVal!" << endl;
exit(1);
}
}
}
//waitpid(id, nullptr, 0); // 为了演示现象就不等待子进程了
// parent process
while(1)
{
cout << "parent process -> globalVal: " << globalVal << endl;
sleep(1);
}
return 0;
}
结论:线程谁先运行与调度器相关。线程一旦异常都有可能导致整个进程整体退出!
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数 return。这种方法对主线程不适用,从main 函数 return 相当于调用 exit。
- 线程可以调用 pthread_ exit 终止自己。
- 一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程。
注:在多线程场景下,不要使用 exit 函数,exit 函数是终止整个进程的!
pthread_exit 函数
- pthread_exit 函数的功能是终止线程。
- retval:retval 不要指向一个局部变量。
- 无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* threadRoutine(void* args)
{
int i = 0;
while(1)
{
cout << "新线程: " << (char*)args << " running ..." << endl;
sleep(1);
if(i++ == 3) break;
}
cout << (char*)args << " quit" << endl;
pthread_exit((void*)10);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");
void* ret = nullptr;
pthread_join(tid, &ret);
cout << "ret: "<< (long long)ret << " main thread wait done... main quit too" << endl;
return 0;
}
pthread_cancel 函数
pthread_cancel 函数的功能是取消一个执行中的线程。thread 是线程的 ID,调用成功是返回 0,失败是返回错误码。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* threadRoutine(void* args)
{
int i = 0;
while(1)
{
cout << "新线程: " << (char*)args << " running ..." << endl;
sleep(1);
}
cout << (char*)args << " quit" << endl;
pthread_exit((void*)13);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");
// pthread_cancel(tid); // 不要一创建线程就取消它
int count = 0;
while(1)
{
cout << "main线程 running ..." << endl;
sleep(2);
count++;
if(count == 5) break;
}
pthread_cancel(tid);
cout << "pthread cancel tid: " << tid << endl;
void* ret = nullptr;
pthread_join(tid, &ret);
cout << "ret: "<< (long long)ret << " main thread wait done... main quit too" << endl;
return 0;
}
当一个线程被取消时,线程的退出结果是 -1(PTHREAD_CANCELED)。使用 pthread_cancel 函数的前提是线程已经跑起来了才能够取消,所以不要穿甲一个线程后就立马取消(可能刚创建的线程还没有跑起来)。一般情况下,都是用主线程来取消新线程的。如果使用新线程来取消主线程的话,这样会影响整个进程。
线程 ID 的深入理解
线程 ID 本质是一个地址!!!因为我们目前用的不是 Linux 自带的创建线程的接口,用的是 pthread 库中的接口!用户需要的是线程,而 Linux 系统只提供轻量级进程,无法完全表示线程,所以在用户和操作系统之间加了个软件层 pthread 库。操作系统承担轻量级进程的调度和内核数据结构的管理,而线程库要给用户提供线程相关的属性字段,包括线程 ID、栈的大小等等。
pthread_self 函数可以获取当前线程的 ID,既然能获得当前线程的 ID,那么线程就可以自己取消自己,但是这种方式不推荐!
线程局部存储:用 __thread 修饰全局变量带来的结果就是让每一个线程各自拥有一个全局变量,这就是线程的局部存储。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
__thread int g_val = 0;
void* threadRoutine(void* args)
{
while(1)
{
cout << (char*)args << " g_val:" << g_val << " &g_val:" << &g_val << endl;
g_val++;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
while(1)
{
cout << "main thread" << " g_val:" << g_val << " &g_val:" << &g_val << endl;
sleep(2);
}
pthread_join(tid, nullptr);
return 0;
}
去掉 __thread 修饰后,所有线程看到的全局变量都是同一个!__thread 所有 pthread 库给 g++ 编译器的一个编译选项!
在多线程的场景下进行进程替换
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
__thread int g_val = 0;
void* threadRoutine(void* args)
{
sleep(5);
execl("/bin/ls", "ls", "-l", nullptr);
while(1)
{
cout << (char*)args << " g_val:" << g_val << " &g_val:" << &g_val << endl;
g_val++;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
while(1)
{
cout << "main thread" << " g_val:" << g_val << " &g_val:" << &g_val << endl;
sleep(1);
}
pthread_join(tid, nullptr);
return 0;
}
在多线程的场景下执行进程替换,那么先会将除主线程外的其它线程都终止掉,然后再进行进程替换。
线程等待
线程在创建并执行的时候,线程也是需要被等待的。如果不等待线程的话,会引起类似于进程的僵尸问题,进而导致内存泄漏。已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。
pthread_join 函数的功能是等待线程结束。thread
是要线程的 ID,retval 指向线程所执行的函数的返回值。调用该函数的线程> 将阻塞等待,直到 ID为 thread 的线程终止。thread 线程以不同的方法终 止,通过 pthread_join 得到的终止状态是不同的,总结如下:
- 如果 thread 线程通过 return 返回,retval 所指向的单元里存放的是 thread 线程函数的返回值。
- 如果 thread 线程被别的线程调用 pthread_ cancel 异常终掉,retval 所指向的单元里存放的是常数
PTHREAD_ CANCELED。- 如果 thread 线程是自己调用 pthread_exit 终止的,retval 所指向的单元存放的是传给 pthread_exit 的参数。
- 如果对 thread 线程的终止状态不感兴趣,可以传 nullptr 给 retval 参数。
- thread 线程函数的返回值不会考虑异常的情况,如果线程出现了异常,那么整个进程都会崩掉。注:状态寄存器是所有线程共享的。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* threadRoutine(void* args)
{
int i = 0;
while(1)
{
cout << "新线程: " << (char*)args << " running ..." << endl;
sleep(1);
if(i++ == 6) break;
}
cout << (char*)args << " quit" << endl;
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");
pthread_join(tid, nullptr); // 默认会阻塞等待
cout << "main thread wait done... main quit too" << endl;
return 0;
}
线程执行的函数的返回值是返回给主线程的,主线程通过该返回值来获取线程退出的状态。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* threadRoutine(void* args)
{
int i = 0;
while(1)
{
cout << "新线程: " << (char*)args << " running ..." << endl;
sleep(1);
if(i++ == 6) break;
}
cout << (char*)args << " quit" << endl;
return (void*)10;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");
void* ret = nullptr;
pthread_join(tid, &ret);
cout << "ret: "<< (long long)ret << " main thread wait done... main quit too" << endl;
return 0;
}
线程执行的函数的返回值可以多种多样,比如返回一段堆空间的起始地址。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* threadRoutine(void* args)
{
int i = 0;
int* ret = new int[7];
while(1)
{
cout << "新线程: " << (char*)args << " running ..." << endl;
sleep(1);
ret[i] = i;
if(i++ == 6) break;
}
cout << (char*)args << " quit" << endl;
return (void*)ret;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");
int* ret = nullptr;
pthread_join(tid, (void**)&ret);
for(int i = 0; i < 7; ++i)
cout << ret[i] << ' ';
cout << endl;
cout << "ret: "<< (long long)ret << " main thread wait done... main quit too" << endl;
return 0;
}
线程分离
- 默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join 是一种负担。这个时候,我们可以告诉系统:当线程退出时,自动释放线程资源,这就是线程分离。
- 一般主线程时不退出的,当用户有个任务要处理,主线程就可以创建新线程来执行用户的任务,但主线程不关心任务处理的结果,那么就可以将该线程分离出去。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cerrno>
#include <cstring>
using namespace std;
__thread int g_val = 0;
void* threadRoutine(void* args)
{
pthread_detach(pthread_self());
while(1)
{
cout << (char*)args << " g_val:" << g_val << " &g_val:" << &g_val << endl;
g_val++;
break;
}
pthread_exit((void*)11);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
while(1)
{
cout << "main thread" << " g_val:" << g_val << " &g_val:" << &g_val << endl;
sleep(1);
break;
}
int n = pthread_join(tid, nullptr);
cout << "n: " << n << " error string: " << strerror(n) << endl;
return 0;
}
注:joinable 和分离是冲突的,一个线程不能既是joinable 又是分离的。
如果线程被分离,但是该线程出现了异常,这样也会影响到整个进程。线程执行的是进程派发的任务,尽管线程被分离了,线程也离不开进程的资源,所以线程出现了异常也会导致进程终止。
注:C++ 11 的线程库也是调用了原生线程库的,所以在使用 C++ 的线程库时也要指定链接原生线程库。
👉总结👈
本篇博客主要讲解了什么是线程、线程和进程的区别、线程的优缺点、线程异常、线程用途以及线程控制等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️