目录
线程在进程内部执行,是操作系统调度的基本单位
线程:有一种执行流, 比进程的执行力度更细, 更轻量化, 创建和终止都更轻, 资源占用更少, 调度成本相对较低
a.资源角度
用户视角: 内核数据结构 + 代码和数据
内核视角: 承担分配系统资源的基本实体(以进程为单位向系统申请资源)
进程 = 一批内核数据结构 + 一个地址空间 + 页表 + 对应的代码数据块
b.CPU角度
--CPU其实不怎么关心,当前是进程还是线程的概念, 只认PCB~~>CPU调用的基本单位"线程"
--LInux下:?PCB <= 其它OS内的PCB ,Linux下的进程, 统一称之为轻量级进程 (CPU拿到的PCB可能是一个独立的进程,也可能是有多个执行流进程的某个PCB)
c.Linux没有真正意义上的线程结构, Linux是用进程的PCB模拟线程的
~~>Linux并不能直接提供给我们线程的接口, 只能提供轻量级进程的接口
~~>在用户层实现了一套多线程方案, 以库的方式提供给用户进行使用(pthread线程库--原生线程库)
a.大部分资源都是共享的, 但寄存器和栈是私有的(重要)
b.进程内的资源一旦释放了, 整个线程也就跟着退出了(线程向进程申请资源)
补充: a.堆区可以被共享,但是我们认为是私有的 b.栈区被认为是私有的
a.调度层面:上下文
b.为什么线程切换的成本更低?
--地址空间 && 页表不需要切换
--CPU内部是有L1~L3cache 对内存的代码和数据, 根据局部原理, 预读CPU内部
--如果进程切换cache就立即失效: 新进程过来, 只能重新缓存
2.0编写的时候加上:lpthread(使用pthread_create,必须引入线程库)
makefile:
mythread:mythread.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f mythread
功能:创建一个新的线程,并在该线程上运行指定的函数
头文件:#include <pthread.h>
函数:int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
返回值:0:成功创建线程,非0:表示错误
thread:一个指向pthread_t类型的指针,用于存储新创建线程的ID;
attr:一个指向pthread_attr_t类型的指针,用于设置新线程的属性(如堆栈大小、调度策略等),
可以传入NULL使用默认属性
start_routine:一个函数指针,指定当新线程被创建时所要执行的函数
arg:传递给start_routine函数的参数
一旦调用成功,新线程将开始执行start_routine函数,并可通过arg参数访问传递给它的数据。线程执行完毕后,可以通过调用pthread_join函数等待线程的终止来进行清理操作
代码编写:
创建多个线程来打印name与pid,若线程是在进程内运行,其pid应该相同
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <cstdio>
using namespace std;
void *ThreadFun(void *args)
{
const string name = (char *)args;
while (1)
{
cout << "name: " << name << " pid: " << getpid() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid[3];
char name[64];
for (int i = 1; i <= 3; i++)
{
snprintf(name,sizeof(name),"%s-%d","thread",i);
pthread_create(tid + i, nullptr, ThreadFun, (void *)name);
sleep(1);
}
while (1)
{
cout << "main thread: " << getpid() << endl;
sleep(3);
}
return 0;
}
结果:
功能:用于等待指定的线程终止
头文件:#include <pthread.h>
函数:int pthread_join(pthread_t thread, void **retval);
返回值:成功返回为0,错误返回非零的错误码
thread:要等待终止的线程的标识符(类型为pthread_t)
retval:用于接收线程的返回值的指针
为什么要等待?
1.exit退出~~>终止进程(一般在线程中不用)
2.pthread_exit函数
头文件:#include<pthread.h>
功能:线程终止
函数:void pthread_exit(void *value_ptr);
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
参数value_ptr:value_ptr不要指向一个局部变量
3.pthread_canel函数
功能:取消一个执行中的线程
原型:int pthread_cancel(pthread_t thread);
返回值:成功返回0;失败返回错误码
参数:thread:线程ID
1.pthread_t 本质就是一个地址
是把线程相关属性集合的起始地址作为线程ID
线程ID是在原生线程库当中的, 该线程匹配的属性集合的起始地址
每个线程要拥有独立的栈(由库去提供), 主线程使用的是内核提供的栈(达到每个线程拥有独立的栈)
可以使用pthread_self() 来获取线程id
2.--thread : 修饰全局变量 ~~> 让每一个线程各自拥有一个全局的变量 -- 线程的局部存储
3.若在线程执行execl函数, 会导致整个线程的代码和数据被替换 等价于进程被替换~~>exit
线程分离后出现异常也会导致整个进程退出
int pthread_detach(pthread_t thread);
临界资源概念补充: 在一个资源被多个执行流共享的情况下, 通过一定的方式,让任何时刻只允许一个执行流访问的资源
--多线程引发的问题
1.因为线程间的切换~~>执行流出现不可预期的结果(调度时序问题) ~~>需要加上多执行流访问资源的保护
--线程不断切换,而对一个不加保护的全局变量做修改的时候可能会引发的问题(如计算)
--当前CPU正在执行哪个执行流,寄存器里面放的就是哪个执行流的上下文数据
~~>当执行流被切换,其上下文是要被保存的
~~>把数据读取到CPU的寄存器, 本质是把我们的数据读取到当前的执行流的上下文
互斥锁(mutex)是一种用于控制多线程对共享资源进行访问的同步机制,在多线程编程中起到了重要的作用。互斥锁的主要功能是确保在任意时刻只有一个线程能够访问共享资源,从而避免竞争条件(race condition)和数据不一致的问题。
1.如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响吗?
2.加锁保护:加锁的时候.一定要保证加锁的粒度,越小越好
3.pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALZER; pthread_mutex_t 就是原生线程库提供的数据类型
4.加锁就是串行执行了吗? 加锁了之后, 线程在临界区中, 是否会切换, 会有问题吗? 原子性的体现
是(执行临界区代码一定是串行的), 会切换, 不会有问题.
第一次理解:虽然被切换了,但是你是持有锁被切换的, 所以其它抢票线程要执行临界区代码, 也必须先申请锁,但锁无法申请成功, 也不会让其它线程进入临界区, 就保证了临界区中数据一致性
//不申请锁,直接访问临界资源 ~~>错误的编码方式
//原子性,在没有持有锁的线程看来,对我有意义的情况只有两种:
1.线程1没有持有锁(什么都没做) 2.线程1释放锁(做完) 此时我可以申请锁
a.可重入VS 线程安全
--常见的线程不安全的情况
--常见的线程安全的情况
--常见不可重入的情况
--常见可重入的情况
b.可重入与线程安全联系
c.可重入与线程安全区别
在C和C++的多线程编程中,可以使用互斥锁来保护共享资源,确保在同一时刻只有一个线程可以访问这些资源。使用互斥锁的一般流程如下:
头文件:<pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex)
函数进行初始化
int pthread_mutex_lock(pthread_mutex_t *mutex)
进行加锁(阻塞式)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
(非阻塞式)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
进行解锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)
进行销毁
//适用于全局变量或者静态变量
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 静态初始化
//适用于在函数内部创建互斥锁对象
pthread_mutex_t mutex; // 变量定义
pthread_mutex_init(&mutex, NULL); // 动态初始化
// 在临界区内加锁和解锁
pthread_mutex_lock(&mutex);
// 访问共享资源
pthread_mutex_unlock(&mutex);
//销毁锁
pthread_mutex_destroy(&mutex);
要访问临界资源,每一个线程必须先申请锁, 每一个线程都必须先看到同一把锁&&访问它~~>锁本身是不是一种临界资源? 谁来保证锁的安全呢?所以为了保证锁的安全, 申请和释放锁必须是原子的自己保证(一行汇编指令)
~swap或exchange指令:以一条汇编的方式,将内存和CPU内寄存区数据进行交换
--在汇编的角度:只有一条汇编语句, 就认为该汇编语句的执行时原子的
--在执行流视角,如何看待CPU上面的寄存器?CPU内部的寄存器本质叫做当前执行流的上下文,寄存器的空间是被所有执行流共享的, 但寄存器的内容,是每一个执行流私有的(上下文)
如果线程A已经申请锁成功, 此时%al: 1, mtx:0, 若现在切换为线程B来申请锁(线程A要带走自己寄存器中的内存:1), 线程B再执行lock的代码, 发现执行失败(%al为0)
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
互斥锁的缺陷: 1.频繁的申请到资源 2.太过于浪费自己和对方的资源
引入同步: 主要是为了解决访问临界资源合理性的问题的
~~>按照一定的顺序,进行临界资源的访问, 线程同步
当我们申请临界资源的时候~~>先要做临界资源是否存在的检测~~>检测的本质:也是访问临界资源
结论: 对临界资源的访问, 也一定是需要在加锁和解锁之间的!
常规方式要检测条件就绪, 注定了我们必须频繁申请和释放锁, 有没有办法让我们的线程检测到资源不就绪的时候(条件变量)
1.不要让线程在频繁的自己检测,等待
2.当条件就绪的时候, 通知对应的进程, 让他来进行资源申请和访问
条件变量是多线程编程中用于线程间通信和同步的一种机制
--条件变量的基本功能包括:等待某个条件的发生(wait)、发送信号通知(signal)和广播通知(broadcast)
头文件:
#include <pthread.h>
初始化:
pthread_cond_init
函数原型:int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
功能:初始化条件变量
参数:cond 是指向条件变量对象的指针,attr 是一个指向线程属性对象的指针,可以为 NULL。
返回值:调用成功返回0,失败返回错误码
销毁:
pthread_cond_destroy
函数原型:int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁条件变量
参数:cond 是指向已初始化的条件变量对象的指针。
返回值:调用成功返回0,失败返回错误码。
等待:
pthread_cond_wait
函数原型:int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
功能:等待条件变量,并在收到信号或广播时解除阻塞
参数:cond 是指向条件变量对象的指针,mutex 是与条件变量相关联的互斥锁。
返回值:调用成功返回0,失败返回错误码。
规范: 在while()中等待-->保证条件就绪的时候再被唤醒
唤醒:
pthread_cond_signal
功能:当条件满足时用来唤醒等待在条件变量上的一个线程。
函数原型:int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒一个等待在条件变量上的线程
参数:cond 是指向条件变量对象的指针。
返回值:调用成功返回0,失败返回错误码
pthread_cond_broadcast
函数原型:int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒所有等待在条件变量上的线程
参数:cond 是指向条件变量对象的指针。
返回值:调用成功返回0,失败返回错误码。
--条件变量通常与互斥锁一起使用:
//pthread_cond_t cond; // 变量定义
//pthread_cond_init(&cond, NULL); // 动态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
等待线程:
pthread_mutex_lock(&mutex);
while (condition_is_not_met)
{
pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);
发送通知的线程:
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);// 改变条件
pthread_mutex_unlock(&mutex);
通过合理使用条件变量,可以实现多线程程序间的同步和通信
避免忙等待的问题,提高系统性能和可维护性
--条件变量通常用于一种生产者-消费者的模式,即一个或多个线程等待某个条件的发生,而另外的线程在某个时刻满足条件后通知等待线程
基本工程师思维
~~>有一部分线程生产对应的数据, 放入缓冲区
~~>有一部分线程消费对应的数据, 对数据做处理
生产者-消费者模型通常需要解决以下几个问题:
通常会使用同步工具,如互斥锁、条件变量或信号量来实现生产者-消费者模型
--生产消费者模型为什么能提高效率 + 解耦?~~>并发
a. 消费者拿走数据花费时间去处理
~~>此时消费者并没有访问仓库和申请锁~~>此时生产者可以生产数据 + 把数据放入到仓库
b.生产者生产数据需要时间去生产
~~>此时生产者并没有访问仓库和申请锁~~>此时消费者可以去拿数据 + 处理数据
生产者生产数据的时候, 消费者可以不去等待生产者生产数据, 仓库里可能有历史的数据, 消费者直接拿去处理
总结: 当生产者在生产数据的时候, 消费者同时也在处理数据~~>两个线程实现的一定程度的并发
通过缓冲区的特点来提高生产和消费的并发度
c.多生产多消费的意义?
当任务很多(生产和消费这个任务所需是的时间较长)
生产之前和消费之后, 它们可以并发的有多个执行流, 同时进行生产和消费
a.信号量是一种软件资源, 信号量本质上是一个计数器:
信号量计数器 ~~> 对临界资源的预定机制
申请信号量 ~~> 计数器 -- ~~> P操作 ~~> 必须是原子的
释放信号量 ~~> 计数器 ++ ~~> V操作 ~~> 必须是原子的
b.计数器的意义:可以不用进入临界区就能知到资源情况(减少临界区内部的判断)
条件变量:申请锁 --> 判断与访问 -->解锁 (本质:我们并不清楚临界资源的情况)
信号量: 提前预设资源的情况, 而且再pv变化过程中, 我们在外部就能知晓临界资源的情况
--头文件:
#include <semaphore.h> // 包含信号量相关的函数和数据类型的声明
#include <pthread.h> // 包含了线程相关的声明,因为通常信号量会和线程一起使用
--初始化sem_init:
函数原型:int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化一个未命名的信号量
参数:
sem 是指向信号量对象的指针,
pshared 用于指示信号量是在进程间共享还是线程间共享,
value 是信号量的初始值。
返回值:调用成功返回0,失败返回-1
--销毁sem_destroy:
函数原型:int sem_destroy(sem_t *sem);
功能:销毁一个未命名的信号量
参数:sem 是指向已初始化的信号量对象的指针
返回值:调用成功返回0,失败返回-1
--等待sem_wait:
函数原型:int sem_wait(sem_t *sem);
功能:等待信号量,如果信号量的值大于0,将其减1;否则将线程阻塞,直到信号量的值大于0
参数:sem 是指向信号量对象的指针
返回值:调用成功返回0,失败返回-1
--发布sem_post:
函数原型:int sem_post(sem_t *sem);
功能:释放信号量,将信号量的值加1,唤醒等待该信号量的线程
参数:sem 是指向信号量对象的指针
返回值:调用成功返回0,失败返回-1
--示例:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define NUM_THREADS 3
sem_t semaphore; // 定义一个信号量
void* thread_function(void* arg) {
int thread_id = *((int*)arg);
printf("Thread %d is waiting...\n", thread_id);
sem_wait(&semaphore); // 等待信号量
printf("Thread %d has acquired the semaphore and is now working\n", thread_id);
// 模拟线程工作
for (int i = 0; i < 5; i++) {
printf("Thread %d is working\n", thread_id);
}
sem_post(&semaphore); // 释放信号量
printf("Thread %d has released the semaphore and finished\n", thread_id);
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
sem_init(&semaphore, 0, 1); // 初始化信号量,初始值为1
int thread_ids[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]);
}
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
sem_destroy(&semaphore); // 销毁信号量
return 0;
}
// 多个线程执行抢票
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int ticket = 50;
void *getTicket(void *args)
{
char *id = (char *)args;
while (1)
{
if (ticket > 0)
{
printf("%s get ticket, rest: %d\n",id,ticket);
ticket--;
}
else
break;
}
}
int main()
{
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, getTicket, (void *)"t1");
pthread_create(&t2, nullptr, getTicket, (void *)"t2");
pthread_create(&t3, nullptr, getTicket, (void *)"t3");
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
return 0;
}
结果:
原因:调度时序问题(进程被切换引起)
ticket>0编译出来可能是3条指令:数据在内存中,而需要通过CPU计算
在执行上面步骤的时候,线程A可能随时被切换,而公共资源ticket又可能线程B拿走(更新ticket),运行一段时间后,线程A切换回来,继续之前的步骤向下计算.~~>引起时序问题
解决:加锁保护临界资源,使其在任意时刻,只能有1个线程来访问
代码:Linux-test: Linux下,提交代码 - Gitee.com
加锁保护临界资源, 线程创建的时候,传递的参数也可以传递自定义对象
创建多个线程打印信息来模拟对临界资源的访问
对临界资源的访问,是需要加锁的,并且我们希望线程按照一定顺序访问临界资源
若临界资源不就绪, 线程就阻塞式等待, 直到被唤醒(不用再频繁的判断临界资源是否就绪)
代码:Linux-test: ---Linux练习代码--- - Gitee.com
角色化:生产者生产数据, 消费者消费数据, 缓冲区:blockqueue
通信: 让生产者和消费者看到同一个阻塞队列
blockqueue:
代码:Linux-test: ---Linux练习代码---
1.使用信号量方案实现生产消费模型--> 可以局部使用临界资源(环形队列)
? ?使用加锁+条件变量实现生产消费模型-->将临界资源看为一个整体
2.当生产者和消费者指向不同位置的时候, 让他们并发执行
当生产者和消费者指向同一个位置的时候, 具有互斥与同步关系即可
3.生产者:关注空间资源, semSpace - > N? ?消费者:关注数据资源,?semData - > 0? ? ?
4.多生产者多消费者
先申请信号量在加锁 (信号量本身时原子的)(多线程并发执行, 先分配完信号量) (后面就只剩进入到锁空间即可) -->效率提高
代码:Linux-test: ---Linux练习代码--- - Gitee.com
1.线程池: 维护一组预先创建的线程来处理任务, 线程池中的线程可以被多个任务重复使用,以减少创合销毁的开销-->提高性能(空间换时间)
2.任务队列: push或pop任务的时候需要加锁(保证资源安全) + 条件变量(避免频繁询问临界资源)
代码:Linux-test: ---Linux练习代码--- - Gitee.com
对一块空间的细粒度划分
struct vm_area_struct?Linux 内核中用于描述虚拟内存区域的结构体
主要包含以下字段:
unsigned long vm_start:虚拟内存区域的起始地址
unsigned long vm_end:虚拟内存区域的结束地址
struct vm_area_struct *vm_next:指向下一个虚拟内存区域的指针
struct vm_area_struct *vm_prev:指向上一个虚拟内存区域的指针
...
可执行程序:exe
1 .exe就是一个文件
2 可执行程序是按照地址空间方式进行编译的
3 可执行程序,按照区域被划分为了以4KB为单位(页帧)
//物理内存也以4KB为单位进行划分(页框)
//IO的基本单位是4KB,IO的时候:将页帧装进页框里
//OS使用struct Page来管理这些4KB
缺页中断:当程序试图访问虚拟内存中的一个页,但是该页不在物理内存中时,就会发生缺页中断。操作系统会响应这个中断,将需要的页从辅助存储(如硬盘)中加载到主内存中,然后重新执行产生中断的指令。
页表的映射