本文要点
- 再次理解页表,了解页表是如何利用虚拟地址进行索引,实现数据读取和传输的
- 了解线程概念,线程的优缺点,线程异常的后果
- 了解线程和进程的差异
- 了解线程库及其基本调用接口(进程创建、终止、等待、控制),学习线程控制的简单示例
- 了解C++对多线程的引入
文章目录
- ☀️一、深入理解页表
- ☀️二、Linux线程概念
- 🌻1.什么是线程(重点)
- ⚡(1)线程的概念
- ⚡(2)线程库初识
- 🌻2.线程的优点
- 🌻3.线程的缺点
- 🌻4.线程异常(重点)
- 🌻5.线程用途
- ☀️三、Linux线程VS进程
- 🌻1.线程和进程
- 🌻2.线程共享
- ☀️四、Linux线程控制
- 🌻1.POSIX线程库
- 🌻2.创建线程
- 🌻3.线程终止
- 🌻4.线程等待
- 🌻5.线程控制代码示例
- ⚡(1)简单版(重点)
- ⚡(2)复杂版
- ☀️五、C++多线程引入
- ☀️结语
☀️一、深入理解页表
知识点1
- 地址空间(task_struct)是进程能看到的资源窗口。
- 页表决定进程真正拥有的资源情况。
- 合理的对地址空间+页表进行资源划分,我们就可以对一个进程的所有资源进行分类。
知识点2
- 磁盘的数据储存单元为
页帧
(4KB),物理内存被划分为一个一个数据页/页框
(4KB)进行管理,磁盘可以将数据加载到内存。 虚拟地址
需要用32位比特位来表示,设计者们有意的将其拆分为10、10、12的位数进行应用,方便对内存空间的映射。- 页表分为
页目录
、页表项
。 页目录
使用虚拟地址的前10位进行索引,指向不同的页表项。页表项
使用虚拟地址的中10位进行索引,指向不同物理内存中的页框的起始地址。操作系统
依据某一页框的起始地址,以虚拟地址后12位作为偏移量,找到该页框内某一具体物理地址。- 在读取或写入某个数据时,可直接根据数据大小,在某一具体物理地址直接向下读取即可。
- 页表根据需要进行创建,不发生映射关系时,对应的数据页表不创建,可大大减少资源占用。
☀️二、Linux线程概念
🌻1.什么是线程(重点)
⚡(1)线程的概念
- 在一个程序里的一个执行路线就叫做
线程(thread)
。更准确的定义是:线程是“一个进程内部的控制序列”。 - 一个进程至少有一个执行线程,可以有多个执行线程。
- 线程在进程内部运行,本质是在进程地址空间内运行,它拥有进程的一部分资源。
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
- 在Linux下,每个线程共享一个地址空间,它们使用 task_strcut类型的数据结构对虚拟内存进行管理,进而对整个进程资源分配。
- 在window下,有为线程专门设计的数据结构(TCB)。
- Linux内核中有没有真正意义的线程呢?没有,Linux是用进程PCB来模拟线程的,是一套完全属于自己的线程方案。
- 站在CPU视角,每一个PCB,都可以将其称之为轻量级进程。
- Linux线程是CPU调度的基本单位,而进程是承担系统资源分配的基本单位。
- 进程用来申请整体资源,线程用来伸手向进程要资源。
- Linux没有真正意义上的线程 - Linux无法直接向外部提供线程的系统调用接口,而只能给我们提供创建轻量级进程的接口(
pthread库
接口)。 - 好处:简单,维护成本低 - 可靠高效(避免数据结构复杂化)。
⚡(2)线程库初识
- 我们可以通过调用用户级线程库(pthread库),让库帮我们访问对应的系统调用接口,创建
轻量级进程
,也就是我们Linux下的线程。 - 任何一款Linux操作系统都会默认携带这个
pthread库
,这种我们将其称之为原生线程库
。
mythread:mythread.cc
g++ -o $@ $^ -lpthread -std=c++11 //-lpthread - 使用pthread库
.PHONY:clean
clean:
rm -f mythread
- 多线程程序运行起来之后
LWP
:light weight process - 轻量级进程id。PID = LWP
,该线程为主线程。PID != LWP
,该线程为新线程。- CPU调度的时候,以LWP为标识符表示特定执行流。
- 创建一个线程 -> 创建PCB -> 把对应的代码分配给它(分配一个函数给它)
🌻2.线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多。
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
- 线程占用的资源要比进程少很多。
- 能充分利用多处理器的可并行数量。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
- 计算密集型应用(加密、解密、算法),为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- I/O密集型应用(外设、网络拉取),为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
重点:与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。1.进程:切换页表 & 虚拟空间地址 & 切换PCB & 切换上下文。2.线程:切换PCB & 切换上下文。
知识补充:CPU内部会集成一个硬件cache,它负责缓存常用的热点数据,当线程切换时,cache不用被切换,CPU/寄存器 可以直接从该集成硬件中直接读取数据。当进程切换时,cache需要被切换。
🌻3.线程的缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性/鲁棒性 降低(重点)
1.编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。2.一个线程出异常,会影响其他线程,因为信号是发给进程整体的,同一个进程内的不同线程共享同一个PID。
- 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高。
编写与调试一个多线程程序比单线程程序困难得多。
🌻4.线程异常(重点)
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
多线程 不等于 多进程(父子进程)
,子进程崩溃不一定会影响父进程运行。
🌻5.线程用途
-
合理的使用多线程,能提高CPU密集型程序的执行效率。
-
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是
多线程运行的一种表现)。
☀️三、Linux线程VS进程
🌻1.线程和进程
- 进程是资源分配的基本单位
- 线程是CPU调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分数据:
线程ID,一组寄存器(上下文),栈,errno,信号屏蔽字,调度优先级
总结:线程一旦被创建,几乎所有资源都是被线程共享的。线程也有自己的私有资源,1.PCB属性私有;2.一定私有的上下文结构;3.每个线程都有自己独立的栈结构。
🌻2.线程共享
进程的多个线程共享同一地址空间,因此Text Segment
(代码段)、Data Segment
(数据段)都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
进程和线程的关系如下图:
☀️四、Linux线程控制
🌻1.POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以
“pthread_”
打头的。 - 要使用这些函数库,要通过引入
头文件<pthread.h>
。 - 链接这些线程函数库时要使用编译器命令的
“-lpthread”
选项。
mythread:mythread.cc
g++ -o $@ $^ -lpthread -std=c++11 //-lpthread - 使用pthread库
.PHONY:clean
clean:
rm -f mythread
🌻2.创建线程
功能:创建一个新的线程
原型
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;失败返回错误码
错误检查
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做),而是将错误代码通过返回值返回。
- pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void* rout(void* arg) {
int i;
for (; ; ) {
printf("I'am thread 1\n");
sleep(1);
}
}
int main(void)
{
pthread_t tid;
int ret;
if ((ret = pthread_create(&tid, NULL, rout, NULL)) != 0) {
fprintf(stderr, "pthread_create : %s\n", strerror(ret)); //打印创建失败信息
exit(EXIT_FAILURE);
}
int i;
for (; ; ) {
printf("I'am main thread\n");
sleep(1);
}
}
🌻3.线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。(return nullptr;)这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用
pthread_ exit
终止自己。 - 一个线程可以调用
pthread_ cancel
终止同一进程中的另一个线程。
- pthread_exit函数
功能:线程终止
原型
void pthread_exit(void* value_ptr);
参数
value_ptr : value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
常见使用方式:
pthread_exit(nullptr);
- pthread_cancel函数
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
🌻4.线程等待
- 线程也是需要被等待的!如果不等待,就会出现类似僵尸进程的问题 - 内存泄漏。
- 即已经退出的线程,其空间没有被释放,仍然在进程的地址空间内,创建新的线程不会复用刚才退出线程的地址空间。、
- 线程等待可以获取线程的退出信息,并回收对应的PCB等内核资源,防止内存泄漏。
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值(void*是一个输出型参数,指向线程的返回值)
返回值:成功返回0;失败返回错误码
常见用法:
void *ret = nullptr;
int n = pthread_join(tid, (void**)&ret);
assert(n == 0);
- 当我们线程
return (void*)n
的时候,pthread库会为我们维护一个小的变量用于保存这个(void*)n
变量,我们可以通过 (void**)&ret 的方法将库中的变量拿出来。 - 我们可以通过下图加深理解,
int **pp = &p,*pp=p,*p=a,若a为我们要取出来的值,则(void**)&p两次解引用即可得到a的值
。
🌻5.线程控制代码示例
⚡(1)简单版(重点)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* thread1(void* arg)
{
printf("thread 1 returning ... \n");
int* p = (int*)malloc(sizeof(int));
*p = 1;
return (void*)p;
}
void* thread2(void* arg)
{
printf("thread 2 exiting ...\n");
int* p = (int*)malloc(sizeof(int));
*p = 2;
pthread_exit((void*)p);
}
void* thread3(void* arg)
{
while (1) { //
printf("thread 3 is running ...\n");
sleep(1);
}
return NULL;
}
int main(void)
{
pthread_t tid;
void* ret;
// thread 1 return
pthread_create(&tid, NULL, thread1, NULL);
pthread_join(tid, &ret);
printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);
free(ret);
// thread 2 exit
pthread_create(&tid, NULL, thread2, NULL);
pthread_join(tid, &ret);
printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);
free(ret);
// thread 3 cancel by other
pthread_create(&tid, NULL, thread3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
if (ret == PTHREAD_CANCELED)
printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n", tid);
else
printf("thread return, thread id %X, return code:NULL\n", tid);
}
运行结果:
[root@localhost linux]# . / a.out
thread 1 returning ...
thread return, thread id 5AA79700, return code:1
thread 2 exiting ...
thread return, thread id 5AA79700, return code : 2
thread 3 is running ...
thread 3 is running ...
thread 3 is running ...
thread return, thread id 5AA79700, return code : PTHREAD_CANCELED
⚡(2)复杂版
复杂版涉及线程循环创建、等待、删除
- makefile
mythread:mythread.cc
g++ -o $@ $^ -lpthread -std=c++11 //-lpthread - 使用pthread库
.PHONY:clean
clean:
rm -f mythread
- mythread.cc
#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 当成结构体使用
class ThreadData
{
public:
int number;
pthread_t tid;
char namebuffer[64];
};
class ThreadReturn
{
public:
int exit_code;
int exit_result;
};
//1. start_routine, 现在是被几个线程执行呢?10, 这个函数现在是什么状态?重入状态
//2. 该函数是可重入函数吗?是的!(可在任意时刻被打断,恢复运行时不会丢失数据)
//3. 在函数内定义的变量,都叫做局部变量,具有临时性 -- 今天依旧适用
// -- 在多线程情况下, 也没有问题 -- 其实每一个线程都有自己独立的栈结构!
void *start_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
int cnt = 10;
while (cnt)
{
cout << "cnt: " << cnt << " &cnt: " << &cnt << endl; // bug
cnt--;
sleep(1);
// return nullptr; // 线程函数结束,return的时候,线程就算终止了
// pthread_exit(nullptr);
// exit(0); // 能不能用来终止线程,不能,因为exit是终止进程的!,任何一个执行流调用exit都会让整个进程退出
}
// 线程如何终止的问题
// return (void*)td->number; // warning, void *ret = (void*)td->number;
// return (void *)106;
// pthread_exit((void*)111);
ThreadReturn * tr = new ThreadReturn();
tr->exit_code = 1;
tr->exit_result = 106;
// ThreadReturn tr; // warning,在栈上开辟的空间 return &tr,退出后数据就拿不到了;
//return (void*)tr; //右值
return (void*)100;
}
int main()
{
// 1. 我们想创建一批线程
vector<ThreadData*> threads;
#define NUM 10
for(int i = 0; i < NUM; i++)
{
ThreadData *td = new ThreadData();
td->number = i+1;
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);//将 "thread", i+1 放入namebuffer
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);
}
for(auto &iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;
}
// 线程是可以被cancel取消的!注意:线程要被取消,前提是这个线程已经跑起来了
// 线程如果是被取消的,退出码:-1 (PTHREAD_CANCELED)
sleep(5);
for(int i = 0; i < threads.size()/2; i++)
{
pthread_cancel(threads[i]->tid); //主线程取消新线程
cout << "pthread_cancel : " << threads[i]->namebuffer << " success" << endl;
}
for(auto &iter : threads) //进程等待
{
void *ret = nullptr; // 注意: 是void *哦
// ? : 为什么没有见到,线程退出的时候,对应的退出信号??? 线程出异常,收到信号,整个进程都会退出!
// pthread_join:默认就认为函数会调用成功!不考虑异常问题,异常问题是你进程该考虑的问题!
int n = pthread_join(iter->tid, (void**)&ret); // void ** retp; *retp = return (void*)td->number
assert(n == 0);
cout << "join : " << iter->namebuffer << " success, exit_code: " << (long long)ret << endl;
delete iter;
}
cout << "main thread quit " << endl;
return 0;
}
- 运行结果
下面是一张另一张结果图哦!
☀️五、C++多线程引入
上面章节讲述的都是以C语言为基础的原生线程库应用,语言设计者们将其加以封装就形成了C++11的多线程。
- 代码示例
#include <iostream>
#include <unistd.h>
#include <thread>
void thread_run()
{
while (true)
{
std::cout << "我是新线程..." << std::endl;
sleep(1);
}
}
int main()
{
// 任何语言,在linux中如果要实现多线程,必定要是用pthread库
// 如何看待C++11中的多线程呢??C++11 的多线程,在Linux环境中,本质是对pthread库的封装!
std::thread t1(thread_run);
while (true)
{
std::cout << "我是主线程..." << std::endl;
sleep(1);
}
t1.join();
return 0;
}
- 运算结果
☀️结语
🌹🌹 多线程1 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪