【Linux系统编程】第四十六弹---线程同步与生产消费模型深度解析
✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】
目录
1、Linux线程同步
1.1、同步概念与竞态条件
1.2、条件变量
1.2.1、认识条件变量接口
1.2.2、举例子认识条件变量
1.2.3、测试代码
2、生产消费模型
2.1、为何要使用生产消费模型
2.2、生产者消费者模型优点
2.3、编写生产消费模型
2.3.1、BlockQueue类基本结构
2.3.2、构造析构函数
2.3.3、判空判满函数
2.3.4、生产者入队
2.3.5、消费者出队
2.4、测试生产消费模型
2.4.1、内置类型
2.4.2、类类型
2.4.3、函数类型
2.4.4、多生产多消费
1、Linux线程同步
在上一弹的上锁抢票代码中我们可以看到,会有很长一段时间使用的是同一个线程,这样的方式没有错,但是不合理,怎么解决这个问题呢?
先通过一个实际情况分析此问题,再解决该问题。
假设学校有一个VIP自习室,一次只允许一个人进来,进入自习室需要用到门口的一把锁。
- 有一个uu今天想去里面自习,就早早5点起床去了VIP自习室,但是他又想,竟然来了就多学习一会,此时外面也有人想进来自习,但是没有钥匙只能在外面等
- 此时这个uu已经学了一上午了,很饿了,想去吃饭,走到门口,刚放回钥匙,又后悔了,如果现在还钥匙了,后面就不能进自习室了,因此这个uu又拿了钥匙进入了自习室(因为uu离钥匙比较近,因此还是他先拿到钥匙)
结论:其他人长时间无法进入自习室 --- 无法获取临界资源 -- 导致饥饿问题!!!
因此我们可以修改规则,让进入自习室更公平!
每一个同学归还钥匙后:
1、不能立马申请
2、第二次申请,必须排队(换句话说,其他人也得排队)
1.1、同步概念与竞态条件
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解
1.2、条件变量
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
1.2.1、认识条件变量接口
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 全局或者静态只需初始化参数:cond:要初始化的条件变量attr:NULL
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);参数:cond:要在这个条件变量上等待mutex:互斥量
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所以线程
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒一个线程
1.2.2、举例子认识条件变量
假设有两个人,一个盘子,一个人放苹果到盘子里,另一个人从盘子里取苹果(前提是有苹果,因此需要先检查是否有苹果),但是互相都不知道什么时候放和取苹果,因此只能一次次的去尝试,是够放好,是否被取,但是这样会导致一个问题,如果一个人不放,那么另一个会一直去检查盘子里有没有苹果,这样就太浪费(线程)资源了,我们可以改进一下策略!!!
优化
我们可以再加一个铃铛,当取苹果的时候,如果盘子里面还没有苹果,那么就可以在铃铛处等待,等另一个人放了苹果了,就来铃铛处通知,这样两个人就能高效利用资源了!!
铃铛就是我们讲解的条件变量:
1.需要一个线程队列
2.需要有通知机制
- 全部叫醒
- 叫醒一个
1.2.3、测试代码
新线程等待函数
const int num = 5;
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;void* Wait(void* args)
{std::string name = static_cast<const char*>(args);while(true){pthread_mutex_lock(&gmutex);pthread_cond_wait(&gcond,&gmutex);usleep(10000);std::cout << "I am " << name << std::endl;pthread_mutex_unlock(&gmutex);}
}
主函数
int main()
{// 1.创建保存线程tid的数组pthread_t threads[num];for(int i=0;i<num;i++){char* name = new char[1024];snprintf(name,1024,"thread-%d",i + 1);pthread_create(threads + i,nullptr,Wait,(void*)name);usleep(1000);}sleep(1);// 2.唤醒其他线程while(true){// pthread_cond_signal(&gcond); // 唤醒一个线程pthread_cond_broadcast(&gcond); // 唤醒所有线程std::cout << "唤醒一个线程..." << std::endl;sleep(2);}// 3.终止线程for(int i=0;i<num;i++){pthread_join(threads[i],nullptr);}return 0;
}
运行结果
2、生产消费模型
2.1、为何要使用生产消费模型
- 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯。
- 所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区(一段内存空间),平衡了生产者和消费者的处理能力。
- 这个阻塞队列就是用来给生产者和消费者解耦的。
2.2、生产者消费者模型优点
- 解耦
- 支持并发
- 支持忙闲不均
思考切入点:"321"原则
- 1、一个交易场所(特定数据结构形式存在的一段内存空间)
- 2、两种角色(生产角色 消费角色)生产线程,消费线程
- 3、三种关系(生产和生产[互斥] 消费和消费[互斥] 生产和消费[同步和互斥])
实现生产消费模型,本质就是通过代码实现321原则,用锁和条件变量(或者其他方式)来实现三种关系!!!
2.3、编写生产消费模型
BlockingQueue
- 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
2.3.1、BlockQueue类基本结构
此处的类设计成模板形式,让结构更加灵活!!!
template<typename T>
class BlockQueue
{
private:bool IsFull();bool IsEmpty();
public:BlockQueue(int cap = defaultcap);// 消费者出队列void Pop(T* out);// 生产者入队列void Equeue(const T& in);~BlockQueue();
private:std::queue<T> _block_queue; // 临界资源int _max_cap;pthread_mutex_t _mutex;pthread_cond_t _p_cond; // 生产着条件变量pthread_cond_t _c_cond; // 消费者条件变量
};
2.3.2、构造析构函数
构造函数用于初始化最大容量和初始化锁以及条件变量,析构函数用于释放锁和条件变量!
// 构造
BlockQueue(int cap = defaultcap) :_max_cap(cap)
{pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_p_cond,nullptr);pthread_cond_init(&_c_cond,nullptr);
}// 析构
~BlockQueue()
{pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_p_cond);pthread_cond_destroy(&_c_cond);
}
2.3.3、判空判满函数
判断是否为空即判断队列是否为空即可,判断是否未满即判断队列成员个数是否与最大容量相等!!
// 判满
bool IsFull()
{return _block_queue.size() == _max_cap;
}
// 判空
bool IsEmpty()
{return _block_queue.empty();
}
2.3.4、生产者入队
入队是将数据插入到队尾中,可能出现数据不一致问题,因此需要加锁和条件变量,如果满了则需要等待,不为满则需要插入数据,并唤醒消费者!!!
// 生产者入队列
void Equeue(const T& in)
{pthread_mutex_lock(&_mutex); // 上锁while(IsFull()){// 满了,生产着不能生产,必须等待// 可是在临界区里面!pthread_cond_wait// 被调用的时候,除了让自己排队等待,还会自己释放传入的锁// 函数返回的时候,不就还在临界区了?// 返回时:必须参与锁的竞争,重新加上锁才能返回pthread_cond_wait(&_p_cond,&_mutex);}// 1.没有满 || 2.被唤醒了_block_queue.push(in); // 生产到阻塞队列pthread_mutex_unlock(&_mutex); // 解锁pthread_cond_signal(&_c_cond); // 唤醒消费者,解锁前解锁后均可
}
2.3.5、消费者出队
出队即删除队头数据,并获取队头的数据,为空则需要等待,不为空则可以删除队头数据,并唤醒生产者!!!
// 消费者出队列
void Pop(T* out)
{pthread_mutex_lock(&_mutex);// 为空,消费者不能消费,必须等待while(IsEmpty()){// 添加尚未满足,但是线程被异常唤醒的情况,叫做伪唤醒!pthread_cond_wait(&_c_cond,&_mutex);}// 1.没有空 || 2.被唤醒*out = _block_queue.front(); // 输出型参数_block_queue.pop();pthread_mutex_unlock(&_mutex);// 唤醒生产着生产pthread_cond_signal(&_p_cond);
}
2.4、测试生产消费模型
2.4.1、内置类型
Consumer
void* Consumer(void* args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);while(true){// 1.获取数据int t;bq->Pop(&t);// 2.处理数据std::cout << "Consumer->" << t << std::endl;}
}
Productor
void* Productor(void* args)
{srand(time(nullptr) ^ getpid());BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);while(true){// 1.构建数据/任务int x = rand() % 10 + 1; // [1,10]sleep(1); // 1秒生产一个数据// 2.生产数据bq->Equeue(x);std::cout << "Productor->" << x << std::endl;}
}
主函数
int main()
{BlockQueue<int>* bq = new BlockQueue<int>();// 单生产 单消费pthread_t c,p;// 创建线程pthread_create(&c,nullptr,Consumer,bq);pthread_create(&p,nullptr,Productor,bq);// 终止线程pthread_join(c,nullptr);pthread_join(p,nullptr);return 0;
}
运行结果
2.4.2、类类型
Task类
设计一个加法的Task类,内部封装仿函数,测试函数!!!
class Task
{
public:Task(){}// 带参构造Task(int x, int y) : _x(x), _y(y){}// 仿函数,直接使用()访问Excute函数void operator()(){Excute();}void Excute(){_result = _x + _y;}std::string debug(){std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=?";return msg;}std::string result(){std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result);return msg;}
private:int _x;int _y;int _result;
};
Consumer
void *Consumer(void *args)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);while (true){// 1.获取数据Task t;bq->Pop(&t);// 2.处理数据// t.Excute();t(); // 使用仿函数std::cout << "Consumer->" << t.result() << std::endl;}
}
Productor
void *Productor(void *args)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);while (true){// 1.构建数据/任务int x = rand() % 10 + 1; // [1,10]usleep(1000); // 尽量保证随机数不同int y = rand() % 10 + 1;Task t(x,y);// 2.生产数据bq->Equeue(t);std::cout << "Productor->" << t.debug() << std::endl;sleep(1);}
}
主函数
int main()
{BlockQueue<Task> *bq = new BlockQueue<Task>();// 单生产 单消费pthread_t c, p;// 创建线程pthread_create(&c, nullptr, Consumer, bq);pthread_create(&p, nullptr, Productor, bq);// 终止线程pthread_join(c, nullptr);pthread_join(p, nullptr);return 0;
}
运行结果
2.4.3、函数类型
函数与声明与实现
// typedef std::function<void()> task_t;
using task_t = std::function<void()>; // 包装器void Download()
{std::cout << "我是一个下载的任务" << std::endl;
}
Consumer
void *Consumer(void *args)
{BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);while (true){// 1.获取数据task_t t;bq->Pop(&t);// 2.处理数据t(); // 使用仿函数}
}
Productor
void *Productor(void *args)
{BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);while (true){// 1.生产数据bq->Equeue(Download);std::cout << "Productor-> Download" << std::endl;sleep(1);}
}
主函数
int main()
{BlockQueue<task_t> *bq = new BlockQueue<task_t>();// 单生产 单消费pthread_t c, p;// 创建线程pthread_create(&c, nullptr, Consumer, bq);pthread_create(&p, nullptr, Productor, bq);// 终止线程pthread_join(c, nullptr);pthread_join(p, nullptr);return 0;
}
运行结果
2.4.4、多生产多消费
int main()
{BlockQueue<task_t> *bq = new BlockQueue<task_t>();// 多生产 多消费pthread_t c1,c2,p1,p2,p3;// 创建线程pthread_create(&c1, nullptr, Consumer, bq);pthread_create(&c2, nullptr, Consumer, bq);pthread_create(&p1, nullptr, Productor, bq);pthread_create(&p2, nullptr, Productor, bq);pthread_create(&p3, nullptr, Productor, bq);// 终止线程pthread_join(c1, nullptr);pthread_join(c2, nullptr);pthread_join(p1, nullptr);pthread_join(p2, nullptr);pthread_join(p3, nullptr);return 0;
}
运行结果
相关文章:

【Linux系统编程】第四十六弹---线程同步与生产消费模型深度解析
✨个人主页: 熬夜学编程的小林 💗系列专栏: 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】 目录 1、Linux线程同步 1.1、同步概念与竞态条件 1.2、条件变量 1.2.1、认识条件变量接口 1.2.2、举例子认识条件变量 1.2.3、…...
VoIP是什么?
IP 语音 (VoIP)(Voice over Internet Protocol) 是一种通过互联网拨打电话的方法。与旧的固定电话系统不同,互联网并非设计用于在连接的人之间实时传输音频信号。必须构建专门的技术和协议才能使之成为可能,这些技术和协议构成了 …...

MySQL 中的集群部署方案
文章目录 MySQL 中的集群部署方案MySQL ReplicationMySQL Group ReplicationInnoDB ClusterInnoDB ClusterSetInnoDB ReplicaSetMMMMHAGalera ClusterMySQL ClusterMySQL Fabric 总结参考 MySQL 中的集群部署方案 MySQL Replication MySQL Replication 是官方提供的主从同步方…...

《设计模式》创建型模式总结
目录 创建型模式概述 Factory Method: 唯一的类创建型模式 Abstract Factory Builder模式 Prototype模式 Singleton模式 最近在参与一个量化交易系统的项目,里面涉及到用java来重构部分vnpy的开源框架,因为是框架的搭建,所以会涉及到像…...

Conda安装与使用中的若干问题记录
Conda安装与使用中的若干问题记录 1.Anaconda 安装失败1.1.问题复述1.2.问题解决(安装建议) 2.虚拟环境pip install未安装至本虚拟环境2.1.问题复述2.2.问题解决 3.待补充 最近由于工作上的原因,要使用到Conda进行虚拟环境的管理,…...

人力资源招聘系统的革新之路:从传统到智能的转变
在全球化与数字化交织的今天,企业间的竞争日益激烈,而人才作为企业发展的核心驱动力,其重要性不言而喻。传统的人力资源招聘方式,如依赖纸质简历、人工筛选、面对面面试等,不仅效率低下,且难以精准匹配企业…...

Python网络爬虫与数据采集实战——网络协议与HTTP
目录 1. HTTP协议简介 2. 常见的请求方法 3. 状态码含义 实际应用中的HTTP协议 1. 如何在爬虫中使用HTTP协议 2. 模拟浏览器请求与爬虫反爬虫技术 3. 高级HTTP请求 实现爬虫时HTTP协议的优化与常见问题 总结 1. HTTP协议简介 HTTP的定义与作用 HTTP(超文本…...

从零开始的c++之旅——二叉搜索树
1、二叉搜索树概念 1. ⼆叉搜索树的概念 ⼆叉搜索树⼜称⼆叉排序树,它或者是⼀棵空树,或者是具有以下性质的⼆叉树: • 若它的左⼦树不为空,则左⼦树上所有结点的值都⼩于等于根结点的值 • 若它的右⼦树不为空,则右⼦树上所有结…...

CSS回顾-基础知识详解
一、引言 在前端开发领域,CSS 曾是构建网页视觉效果的关键,与 HTML、JavaScript 一起打造精彩的网络世界。但随着组件库的大量涌现,我们亲手书写 CSS 样式的情况越来越少,CSS 基础知识也逐渐被我们遗忘。 现在,这种遗…...

Elasticsearch 查询时 term、match、match_phrase、match_phrase_prefix 的区别
Elasticsearch 查询时 term、match、match_phrase、match_phrase_prefix 的区别 keyword 与 text 区别term 查询match 查询match_phrase 查询match_phrase_prefix 查询写在最后 在讲述 es 查询时 term、match、match_phrase、match_phrase_prefix 的区别之前,先来了…...
低代码平台:跨数据库处理的重要性与实现方式
一、低代码平台概述 低代码平台作为一种创新的软件开发工具,为开发者带来了极大的便利。它具备可视化编程工具和大量预构建组件,这使得开发者无需编写大量代码就能创建应用程序,显著降低了软件开发的技术门槛。无论是专业开发人员还是业务人员…...
【jvm】如何破坏双亲委派机制
目录 1.说明2.重写ClassLoader的loadClass方法2.1 原理2.2 实现步骤2.3 注意事项 3.使用线程上下文类加载器3.1 原理3.2 实现步骤3.3 应用场景 4.利用SPI机制4.1 原理4.2 实现步骤4.3 应用场景 5.Tomcat等容器的自定义类加载器5.1 原理5.2 实现方式5.3 应用场景 1.说明 1.双亲委…...

ReactPress与WordPress:一场内容管理系统的较量
ReactPress Github项目地址:https://github.com/fecommunity/reactpress WordPress官网:https://wordpress.org/ ReactPress与WordPress:一场内容管理系统的较量 在当今数字化时代,内容管理系统(CMS)已成为…...

网络安全练习之 ctfshow_web
文章目录 VIP题目限免(即:信息泄露题)源码泄露前台JS绕过协议头信息泄露robots后台泄露phps源码泄露源码压缩包泄露版本控制泄露源码(git)版本控制泄露源码2(svn)vim临时文件泄露cookie泄露域名txt记录泄露敏感信息公布内部技术文档泄露编辑器…...
在 Service Worker 中caches.put() 和 caches.add()/caches.addAll() 方法他们之间的区别
在 Service Worker 中,caches.put(request, response) 和 caches.add(request)/caches.addAll(requests) 方法都用于将资源添加到缓存中,但它们的使用场景和目的略有不同。 caches.put(request, response),一用在fetch事件当中,由…...

UNIAPP发布小程序调用讯飞在线语音合成+实时播报
语音合成能够将文字转化为自然流畅的人声,提供100发音人供您选择,支持多语种、多方言和中英混合,可灵活配置音频参数。广泛应用于新闻阅读、出行导航、智能硬件和通知播报等场景。 在当下大模型火爆的今日,语音交互页离不开语音合…...

跳房子(弱化版)
题目描述 跳房子,也叫跳飞机,是一种世界性的儿童游戏,也是中国民间传统的体育游戏之一。 跳房子的游戏规则如下: 在地面上确定一个起点,然后在起点右侧画 n 个格子,这些格子都在同一条直线上。每个格子内…...
ubuntu22 安装 minikube
在Ubuntu 22上安装Minikube,你可以按照以下步骤进行: 安装依赖: 更新系统并安装必要的依赖项: sudo apt-get update sudo apt-get install -y apt-transport-https ca-certificates curl安装Docker: Minikube可以使用D…...
STM32 | 超声波避障小车
超声波避障小车 一、项目背题 由于超声波测距是一种非接触检测技术,不受光线、被测对象颜色等的影响,较其它仪器更卫生,更耐潮湿、粉尘、高温、腐蚀气体等恶劣环境,具有少维护、不污染、高可靠、长寿命等特点。因此可广泛应用于…...

打造旅游卡服务新标杆:构建SOP框架与智能知识库应用
随着旅游业的蓬勃兴起,旅游卡产品正逐渐成为市场的焦点。为了进一步提升服务质量和客户体验,构建一套高效且标准化的操作流程(SOP)变得尤为重要。本文将深入探讨如何构建旅游卡的SOP框架,并介绍如何利用智能知识库技术…...

MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...

大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...

华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...

论文浅尝 | 基于判别指令微调生成式大语言模型的知识图谱补全方法(ISWC2024)
笔记整理:刘治强,浙江大学硕士生,研究方向为知识图谱表示学习,大语言模型 论文链接:http://arxiv.org/abs/2407.16127 发表会议:ISWC 2024 1. 动机 传统的知识图谱补全(KGC)模型通过…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...

嵌入式学习笔记DAY33(网络编程——TCP)
一、网络架构 C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务…...

免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...
2025年低延迟业务DDoS防护全攻略:高可用架构与实战方案
一、延迟敏感行业面临的DDoS攻击新挑战 2025年,金融交易、实时竞技游戏、工业物联网等低延迟业务成为DDoS攻击的首要目标。攻击呈现三大特征: AI驱动的自适应攻击:攻击流量模拟真实用户行为,差异率低至0.5%,传统规则引…...

【大模型】RankRAG:基于大模型的上下文排序与检索增强生成的统一框架
文章目录 A 论文出处B 背景B.1 背景介绍B.2 问题提出B.3 创新点 C 模型结构C.1 指令微调阶段C.2 排名与生成的总和指令微调阶段C.3 RankRAG推理:检索-重排-生成 D 实验设计E 个人总结 A 论文出处 论文题目:RankRAG:Unifying Context Ranking…...