欢迎来到Cefler的博客😁
🕌博客主页:折纸花满衣
🏠个人专栏:题目解析
🌎推荐文章:【LeetCode】winter vacation training
目录
👉🏻线程概念
🌈理论概念
在Linux系统中,线程是指在同一进程内并发执行的一组指令序列。线程共享同一进程的地址空间、文件描述符等资源,但每个线程有自己的栈空间和寄存器集,使得线程之间可以独立运行,实现并发执行。
Linux系统使用轻量级进程(LWP)来实现线程,也就是说,每个线程都被视为一个独立的轻量级进程。这种设计使得线程的创建和切换开销较小,能够更高效地利用系统资源。
在Linux中,线程的创建和管理通常通过POSIX线程库(pthread)来实现。开发者可以使用pthread库中提供的函数来创建线程、控制线程的执行顺序、进行线程同步等操作。
🌈通俗理解线程
1.线程是比进程更加轻量化的一种执行流
/线程是在进程内部执行的一种执行流
2.线程是CPU调度的基本单位/进程是承担系统资源的基本实体
进程的创建很麻烦,要构建进程地址空间,页表,加载代码数据、动静态库,建立映射关系等多项操作,而线程只要创建一个描述PCB即可,因为线程在进程的地址空间中运行,前人栽树后人乘凉,只用共享进程地址空间中的资源就好了。
在linux下,线程就是轻量级进程,我们以前学的进程,是一个进程内只有一个执行流,而现实是,一个进程内会有多个执行流,进程创建初始会伴有一个初始PCB,后面产生多个执行流后再根据先描述再组织创建对应的PCB,每个PCB之间的地位相同
👉🏻线程优缺点
线程作为一种并发编程的重要工具,具有许多优点和缺点。以下是线程的主要优缺点总结:
优点:
- 资源共享:线程可以方便地共享同一进程的资源,如内存空间、文件描述符等,减少资源开销和提高效率。
- 响应速度快:线程之间切换的开销较小,能够实现快速的响应和处理多任务。
- 并发性能:线程可以利用多核处理器实现并行计算,提高系统的性能和吞吐量。
- 简化编程:使用线程可以简化程序设计,将复杂任务拆分为多个线程并发执行,提高程序的可维护性和扩展性。
缺点:
5. 竞态条件:线程间的并发执行可能导致竞态条件(Race Condition),需要使用同步机制(如互斥锁、信号量)来解决,增加了编程复杂性。
6. 死锁:线程间的相互等待资源导致的死锁是并发编程中常见的问题,需要仔细设计和管理线程之间的依赖关系。
7. 调试困难:多线程程序的调试和测试比单线程程序更为复杂,因为涉及到线程间的交互和并发执行。
8. 资源消耗:每个线程都需要独立的栈空间和线程上下文,可能会占用较多的系统资源,限制了线程数量的上限。
9. 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
👉🏻创建线程(代码示例)
以下是一个使用 pthread.h
头文件和 pthread_create
函数创建线程的 C++ 示例代码:
#include <iostream>
#include <pthread.h>
// 线程函数,打印一段文字
void* printMessage(void* arg) {
for(int i = 0; i < 5; ++i) {
std::cout << "Hello, I am a thread!" << std::endl;
}
return nullptr;
}
int main() {
pthread_t tid;
// 创建一个新线程,并指定线程函数为 printMessage
int ret = pthread_create(&tid, nullptr, printMessage, nullptr);
if (ret != 0) {
std::cerr << "Error creating thread: " << ret << std::endl;
return 1;
}
// 主线程继续执行自己的任务
for(int i = 0; i < 3; ++i) {
std::cout << "Hello from the main thread!" << std::endl;
}
// 等待子线程执行完毕
pthread_join(tid, nullptr);
return 0;
}
在这个示例中,我们使用 pthread.h
头文件中的 pthread_create
函数来创建一个新线程。主线程和子线程分别打印不同的文本内容,最后主线程调用 pthread_join
函数等待子线程执行完毕。希望这个示例对您有所帮助!
以下是 pthread_create
函数和 pthread_join
函数的简要介绍:
🍒pthread_create 函数
pthread_create
函数用于创建一个新的线程,并指定线程函数。它的原型如下:
#include <pthread.h>
int pthread_create(pthread_t* thread, const pthread_attr_t* attr,
void* (*start_routine)(void*), void* arg);
thread
:一个指向pthread_t
类型变量的指针,用于存储新线程的 ID;attr
:一个指向pthread_attr_t
类型变量的指针,用于设置线程属性(通常指定为nullptr
);start_routine
:一个指向线程函数的指针,该函数在新线程中执行;arg
:一个指针参数,传递给线程函数作为参数。
pthread_create
函数返回 0 表示成功,否则表示失败。
🍒pthread_join 函数
pthread_join
函数用于等待一个线程执行完毕,并回收其资源。它的原型如下:
#include <pthread.h>
int pthread_join(pthread_t thread, void** retval);
thread
:待等待的线程的 ID;retval
:一个指向指针的指针,用于存储线程函数的返回值(通常指定为nullptr
)。
pthread_join
函数阻塞当前线程,直到指定的线程执行完毕。如果指定的线程已经结束,那么 pthread_join
函数会立即返回。成功返回 0,否则返回一个非零值表示出错。
LWP(轻量级进程)
LWP(Lightweight Process,轻量级进程)是一种在类 Unix 操作系统中用于实现线程的机制。在这种机制下,每个进程可以包含多个轻量级进程(LWP),每个 LWP 都有自己的程序计数器、栈、寄存器和状态。LWP 通常由操作系统内核直接管理,因此比传统的线程更轻量级。
LWP 的优点包括:
- 更快的线程创建和上下文切换:由于 LWP 是由操作系统内核管理的,因此线程的创建和上下文切换开销较小。
- 更好的并发性能:LWP 可以更有效地利用多核处理器和多处理器系统,提高并发性能。
LWP 在许多类 Unix 系统中被广泛使用,例如 Solaris、FreeBSD 和 AIX 等系统。它们通常与用户级线程库(如 POSIX 线程库)结合使用,通过将用户级线程映射到轻量级进程来实现线程并发。
这里顺便科普一下TTY
TTY(Teletype,电传打字机)是指一种用于与计算机进行交互的设备或接口。在早期的计算机系统中,TTY通常指代终端设备,用户可以通过终端设备与计算机进行交互,输入命令并查看输出结果。随着技术的发展,TTY这个术语逐渐演变为代表任何与计算机进行交互的设备、接口或会话。
在类 Unix 系统中,TTY通常指代以下几种内容之一:
- 物理终端设备:早期的计算机系统使用物理终端设备(如电传打字机或显示器)作为用户与计算机进行交互的工具。每个物理终端设备都被称为一个TTY。
- 虚拟终端设备:在类 Unix 系统中,虚拟终端设备也被称为TTY。在多用户环境下,每个用户登录到系统后通常会分配一个虚拟终端设备,用于与系统进行交互。
- 伪终端设备:伪终端设备(pseudo-TTY,PTY)是一种用于在用户进程和程序之间建立通信连接的设备接口,例如用于在终端模拟器中运行的 shell 会话。
总之,TTY是一个广义的术语,指代用于与计算机进行交互的设备、接口或会话,包括物理终端设备、虚拟终端设备和伪终端设备等。
👉🏻如何理解线程为轻量级进程
线程被称为轻量级进程是因为它相比于传统的进程来说具有以下几个轻量级的特点:
-
资源开销低:线程在创建、切换和销毁时所需的资源开销相对较低。与进程相比,线程共享了同一进程的地址空间、文件描述符等资源,不需要像进程那样独立拥有自己的资源副本,因此节约了内存和系统资源。
-
切换快速:由于线程共享进程的地址空间,在进行线程切换时只需保存和恢复少量的上下文信息,切换速度较快。而进程之间的切换需要保存和恢复更多的上下文信息,因此较慢。
-
通信方便:线程之间可以通过共享内存等方式轻松地进行通信,不需要像进程那样使用进程间通信(IPC)的机制来进行通信,因此通信成本更低。
-
并发性高:由于线程共享进程的资源,线程之间的通信和同步更加方便快捷,能够更好地实现并发执行,提高系统的性能和响应速度。
-
灵活性强:线程的创建和销毁比进程更加灵活,可以根据需要动态地创建和销毁线程,适应不同的任务需求,提高系统的灵活性和效率。
总的来说,线程作为轻量级进程体现在资源开销低、切换快速、通信方便、并发性高和灵活性强等方面。这些特点使得线程在实现并发编程时更加高效和灵活,适用于各种需要并发处理的场景。
cache(缓存)
在 CPU 调度中,缓存(cache)是一个非常重要的概念。CPU 的缓存通常分为三级:一级缓存(L1 Cache)、二级缓存(L2 Cache)和三级缓存(L3 Cache)。缓存的存在主要是为了加快 CPU 对数据的访问速度,提高计算效率。
在 CPU 调度中,缓存对性能有着重要影响,主要体现在以下几个方面:
- 命中率:缓存命中率是衡量缓存性能的重要指标。当 CPU 需要访问数据时,如果该数据在缓存中已经存在,则称为缓存命中,可以直接从缓存中读取数据,避免访问主存,提高访问速度。高缓存命中率可以减少对主存的访问次数,提高整体性能。
- 缓存替换策略:当缓存空间不足时,需要根据一定的策略决定哪些数据会被替换出去。常见的缓存替换策略包括最近最少使用(LRU)、先进先出(FIFO)等。合理的缓存替换策略可以提高缓存的利用率,减少缓存未命中带来的性能损失。
- 缓存一致性:多核 CPU 中,每个核心都有自己的缓存,可能导致缓存之间的数据不一致。为了保持数据的一致性,需要使用一致性协议(如 MESI 协议)来协调不同核心之间的数据更新和同步操作。
- 缓存预取:为了提高缓存命中率,CPU 会预先将可能需要访问的数据加载到缓存中。通过智能的缓存预取策略,可以减少访存延迟,提高程序执行效率。
👉🏻页框和页帧
文件系统IO的基本单位是:4KB
页框和页帧通常用于描述虚拟内存管理中的概念。
页框
(page frame)是指物理内存中的固定大小的块,用于存储进程的数据和代码。这些页框的大小通常是固定的,比如4KB或者8KB。操作系统会将进程的虚拟内存空间映射到物理内存中的页框上。
页帧
(page)是指进程的虚拟地址空间中的固定大小的块,也就是虚拟内存中的页面。当进程访问虚拟内存中的某个页时,操作系统会将这个页映射到物理内存中的一个页框上,这样进程就可以访问对应的数据或者代码了。
通过页框和页帧的映射,操作系统可以实现虚拟内存管理,允许多个进程共享有限的物理内存,并且可以在不同的页帧之间进行动态的映射和调度,以便更高效地利用系统资源。
👉🏻页目录
页目录(page directory)是在虚拟内存管理中用于实现页表的一种数据结构。在x86架构的操作系统中,虚拟内存被分割成固定大小的页面(page),每个页面通常为4KB。
页目录是一个数据结构,用于存储虚拟地址空间中每一页的映射信息。在x86架构下,页目录的每个条目称为页目录项(page directory entry),每个页目录项对应着一段虚拟地址空间中的页表。页目录项中包含了指向页表的指针,以及控制页面权限和其他属性的标志位。
通过页目录和页表的组合,操作系统可以实现虚拟内存到物理内存的映射。当程序访问虚拟内存中的某个地址时,CPU会根据页目录和页表的映射关系,将虚拟地址转换成物理地址,从而实现内存的访问。
划分页表的本质:划分地址空间!
总的来说,页目录是虚拟内存管理的重要组成部分,它提供了虚拟地址到物理地址的映射关系,帮助操作系统实现对内存的有效管理和保护。
👉🏻创建单线程代码
Makefile
pthread:pthread.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm -f pthread
pthread.cc
#include<iostream>
#include<string>
#include<unistd.h>
#include<pthread.h>
#include<functional>
#include<time.h>
using namespace std;
using func_t = function<void()>;//等价于typedqef funciton<void()> func_t
class ThreadData
{
public:
ThreadData(const string& name,const uint64_t &ctime,func_t f)
:threadname(name)
,createname(ctime)
,func(f)
{}
string threadname;
uint64_t createname;
func_t func;
};
void Print()
{
cout<<"我是线程执行总任务的一部分"<<endl;
}
void* ThreadRountine(void* args)
{
string threadname = (const char*)args;
ThreadData* td = static_cast<ThreadData*>(args);//强转
while(true)
{
cout<<"new thread"<<" thread name:"<<td->threadname<<" create time: "<<td->createname<<endl;
td->func();
sleep(1);
}
}
int main()
{
pthread_t tid;
ThreadData* td = new ThreadData("thead 1",(uint64_t)time(nullptr),Print);
pthread_create(&tid,nullptr,ThreadRountine,td);
while(true)
{
cout<<"main thread"<<endl;
sleep(3);
}
return 0;
}
那么如果我想创建多线程呢?
#include <iostream>
#include <string>
#include <functional>
#include <vector>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
// typedef std::function<void()> func_t;
using func_t = std::function<void()>;
const int threadnum = 5;
class ThreadData
{
public:
ThreadData(const std::string &name, const uint64_t &ctime, func_t f)
: threadname(name), createtime(ctime), func(f)
{
}
public:
std::string threadname;
uint64_t createtime;
func_t func;
};
void Print()
{
std::cout << "我是线程执行的大任务的一部分" << std::endl;
}
// 新线程
void *ThreadRountine(void *args)
{
int a = 10;
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
std::cout << "new thread"
<< " thread name: " << td->threadname << " create time: " << td->createtime << std::endl;
td->func();
if(td->threadname == "thread-4")
{
std::cout << td->threadname << " 触发了异常!!!!!" << std::endl;
// a /= 0; // 故意制作异常
}
sleep(1);
}
}
// 如何给线程传参,如何创建多线程呢??? -- done
// 研究两个问题: 1. 线程的健壮性问题 2. 观察一下thread id
// 获取返回值
// 主线程
int main()
{
std::vector<pthread_t> pthreads;
for (size_t i = 0; i < threadnum; i++)
{
char threadname[64];
snprintf(threadname, sizeof(threadname), "%s-%lu", "thread", i);
pthread_t tid;
ThreadData *td = new ThreadData(threadname, (uint64_t)time(nullptr), Print);
pthread_create(&tid, nullptr, ThreadRountine, td);
pthreads.push_back(tid);
sleep(1);
}
std::cout << "thread id: ";
for(const auto &tid: pthreads)
{
std::cout << tid << ",";
}
std::cout << std::endl;
while (true)
{
std::cout << "main thread" << std::endl;
sleep(3);
}
}
👉🏻线程补充小知识
1.操作系统的最小调度单位是线程
2.进程是资源的分配单位,所以线程并不拥有系统资源,而是共享使用进程的资源,进程的资源由系统进行分配
3.任何一个线程都可以创建或撤销另一个线程
4.pthread_self() 用于获取用户态线程的tid,而并非轻量级进程ID
线程的tid和轻量级进程的ID有什么区别?
线程的 tid
(Thread ID)和轻量级进程的ID(LWP ID)是用来标识线程和轻量级进程(在支持多线程的操作系统中,线程通常被视为轻量级进程)的两种不同标识符。它们之间的区别如下:
-
线程的
tid
(Thread ID):tid
是用来唯一标识一个线程的标识符。- 每个线程都有自己的
tid
,通过tid
可以唯一标识和操作特定的线程。 - 在多线程编程中,
tid
通常用于线程管理、同步和通信。
-
轻量级进程的ID(LWP ID):
- 轻量级进程ID是在支持多线程的操作系统中用于标识线程或轻量级进程的标识符。
- 在某些操作系统中,每个线程被视为一个独立的轻量级进程(LWP),每个LWP都有自己的LWP ID。
- LWP ID通常用于在内核级别标识和管理线程、进行调度、监控和同步。
主要区别:
tid
是线程级别的标识符,用于唯一标识一个线程;而 LWP ID 是内核级别的标识符,用于标识线程或轻量级进程。- 每个线程都有自己的
tid
,但在某些系统中,多个线程可能共享同一个LWP ID。 tid
更加面向用户空间,用于用户程序对线程的管理;而 LWP ID 更加面向内核空间,用于内核对线程的管理和调度。
总之,tid
是线程级别的标识符,而 LWP ID 是内核级别的标识符,用于标识线程或轻量级进程。在多线程编程和操作系统内核中,了解并区分这两种标识符对于理解和管理线程非常重要。
- LWP是轻量级进程,在Linux下进程是资源分配的基本单位,线程是cpu调度的基本单位,而线程使用进程pcb描述实现,并且同一个进程中的所有pcb共用同一个虚拟地址空间,因此相较于传统进程更加的轻量化
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长