深入浅出WebRTC—NACK
WebRTC 中的 NACK(Negative Acknowledgment)机制是实时通信中处理网络丢包的关键组件。网络丢包是常见的现象,尤其是在无线网络或不稳定连接中。NACK 机制旨在通过请求重传丢失的数据包来减少这种影响,从而保持通信的连续性和质量。
1. 总体架构
WebRTC NACK 总体架构如下图所示。
1)发送端发送 RTP 报文时会缓存一份到 RtpPacketHistory,收到 NACK 请求的时候从 RtpPacketHistory 获取对应缓存报文发送出去。RtpPacketHistory 收到 TransportFeedback 会将接收端确认的报文从缓存中移除。
2)接收端收到的所有报文都从 NackRequester 过一遍(只需要序列号),丢失了哪个报文门清。由于丢包和乱序无法分辨,NackRequest 在定时器驱动下发送 NACK 请求(极端丢包情况会发送关键帧请求)。
2. 发送端
2.1. 调用流程
发送端的调用流程由三条子流程组成:
1)发送出去的报文会被缓存到 RtpPacketHistory,用来响应 NACK 请求。
2)收到 TransportFeedback 将对应报文从 RtpPacketHistory 中移除。
3)收到 NACK 请求从 RtpPacketHistory 获取缓存报文发送出去。
2.2. RtpPacketHistory
RtpSenderEgress 负责报文发送,发送完后将报文缓存到 RtpPacketHistory。ModuleRtpRtcpImpl2 处理所有 RTCP 报文,NACK 请求交给 RTPSender 处理,RTPSender 从 RtpPacketHistory 获取请求重传的报文然后发送出去。
2.2.1. 重传条件
RtpSenderEgress 只会将满足条件的报文缓存到 RtpPacketHistory。正常的视频帧需要重传,但 FEC 报文不重传。另外,对于 simulcast 或 SVC,需要根据重传策略来决定,判断逻辑比较复杂,这里暂不分析。
void RtpSenderEgress::CompleteSendPacket(const Packet& compound_packet,bool last_in_batch) {...if (is_media && packet->allow_retransmission()) {packet_history_->PutRtpPacket(std::make_unique<RtpPacketToSend>(*packet), now);} else if (packet->retransmitted_sequence_number()) {packet_history_->MarkPacketAsSent(*packet->retransmitted_sequence_number());}...
}
2.2.2. 队列长度
缓存队列长度非常重要,太长的话,会引入较大延迟,太短的话,会导致重传 miss。因此,队列长度的设置需要在延迟和 miss 之间取得一个较好的平衡。
WebRTC 从时间和数量两个维度来对队列长度进行限制,其中,kMaxCapacity 是一个硬性数量限制,不管缓存的报文是否新鲜,都不能超过这个限制。
// packet_duration = max(1 second, 3x RTT).
static constexpr TimeDelta kMinPacketDuration = TimeDelta::Seconds(1);
static constexpr int kMinPacketDurationRtt = 3;// With kStoreAndCull, always remove packets after 3x max(1000ms, 3x rtt).
static constexpr int kPacketCullingDelayFactor = 3;// number_to_store_ = min(kMaxCapacity, kMinSendSidePacketHistorySize)
static constexpr size_t kMaxCapacity = 9600;
static const int kMinSendSidePacketHistorySize = 600;
void RtpPacketHistory::CullOldPackets()
{// 当前时间Timestamp now = clock_->CurrentTime();// 取 3 倍 RTT 和 1秒两者较大值,即不小于 1 秒TimeDelta packet_duration =rtt_.IsFinite()? std::max(kMinPacketDurationRtt * rtt_, kMinPacketDuration): kMinPacketDuration;while (!packet_history_.empty()) {// 队列中报文数量超过最大容量限制if (packet_history_.size() >= kMaxCapacity) {RemovePacket(0); // 移除最旧的报文continue;}// 取队列首报文进行判断const StoredPacket& stored_packet = packet_history_.front();// 正在重传中,退出if (stored_packet.pending_transmission_) {return;}// 还很新鲜(未超时),退出if (stored_packet.send_time() + packet_duration > now) {return;}// 首报文已经不新鲜,如果报文数量多或者首报文太老,才需要移除if (packet_history_.size() >= number_to_store_ ||stored_packet.send_time() + (packet_duration * kPacketCullingDelayFactor) <= now) {RemovePacket(0);} else {// No more packets can be removed right now.return;}}
}
2.2.3. PaddingMode
RtpPacketHistory 还可以用来生成带宽探测所需的 padding 报文,用真实报文当 padding 报文,既填充了码率又实现了冗余,一石二鸟。
RtpPacketHistory 中缓存了很多报文,挑选哪些报文做 padding 报文,支持三种 padding 模式:
enum class PaddingMode {// 选择最近缓存的报文作为 Padding 报文kDefault,// 基于发送时间、重传次数等因素选择更好的历史报文作为 Padding 报文kPriority,// 使用最近缓存的大包作为Padding报文kRecentLargePacket
};
对于 kPriority 模式,优先级定义如下:
bool RtpPacketHistory::MoreUseful::operator()(StoredPacket* lhs,StoredPacket* rhs) const {// 没有重传过的报文优先级更高if (lhs->times_retransmitted() != rhs->times_retransmitted()) {return lhs->times_retransmitted() < rhs->times_retransmitted();}// 时间越近的报文优先级越高return lhs->insert_order() > rhs->insert_order();
}
最新代码已经不再使用 kDefault 模式。
RtpPacketHistory::PaddingMode GetPaddingMode(const FieldTrialsView* field_trials) {if (!field_trials ||!field_trials->IsDisabled("WebRTC-PaddingMode-RecentLargePacket")) {return RtpPacketHistory::PaddingMode::kRecentLargePacket;}return RtpPacketHistory::PaddingMode::kPriority;
}
3. 接收端
3.1. 调用流程
NackRequester 是接收端的 NACK 控制核心,调用流程如下图所示。
1)RtpVideoStreamReceiver2 收到报文,在进行处理的同时也要通知 NackRequester。
2)NackRequester 内部有一个 NACK 请求队列,如果发现有丢包就会添加一个 NACK 请求项。
3)NackPeriodicProcessor 会定时调用 NackRequester 发送 NACK 请求。
4)通过层层调用将 NACK 请求发送出去。
3.2. NackRequester
每一个 RtpVideoStreamReceiver2 都持有一个 NackRequester,用来发起 NACK 请求。NackRequester 被 NackPeriodicProcessor 定时驱动,NACK 请求通过 NackSender 发送出去。如果丢包特别严重,NackRequester 会使用 KeyFrameRequestSender 发起关键帧请求。
3.2.1. NackList
NackList 是 NackRequester 内部的 NACK 请求队列。每次收到新的报文,与最近收到的报文 SN 进行比较,如果两个 SN 之间有空洞(SN 跳跃),认为有丢包,以空洞 SN 创建 NACK 请求项插入 NackList。
// 队列中首尾报文Sequence Number的最大跨度,适用于NackList、KeyFrameList和RecoveredList
constexpr int kMaxPacketAge = 10'000;
// 队列中最大报文数
constexpr int kMaxNackPackets = 1000;
// 最大重传次数
constexpr int kMaxNackRetries = 10;
因为空洞也可能是乱序导致,后续可能立即就会收到丢失报文,所以不能立即发送 NACK 请求。WebRTC 会启动一个定时器,确定 NackRequester 定时检查 NackList 中的 NACK 项,判断是否需要发送 NACK 请求。
决定选取哪些 NACK 项发起 NACK 请求,有不同筛选条件:
enum NackFilterOptions { kSeqNumOnly, kTimeOnly, kSeqNumAndTime };
1)kSeqNumOnly
基于报文乱序情况,每个 NACK 项插入队列时都会计算一个触发重传的 SN,表示后续收到此 SN 报文时,如果NACK 项还在队列中,且还没有发起过 NACK 请求,则立即触发一次。
每收到一个报文会检查此条件,当瞬时丢包比较严重的时候,能够比定时器更快触发 NACK 请求的发送,类似于 TCP 的快速重传机制。
2)kTimeOnly
每次发送 NACK 请求都会更新 NACK 的最近请求时间,如果最近请求时间距当前时间超过一个 RTT,则会重新触发 NACK 请求。此条件由定时器驱动进行检查。
3)kSeqNumAndTime
相当于“kSeqNumOnly || kTimeOnly”,只要一个条件满足就会触发 NACK 请求。(好像未使用)
3.2.2. KeyFrameList
KeyFrameList 存储每个关键帧的第一个报文,用来协助 NackList 进行收缩。对于视频来说,GOP 中的帧是有依赖关系的,如果前面的帧没有恢复,恢复后面的帧没有意义。因此,当 NackList 请求项溢出需要移除一些腾出空间时,WebRTC 是按照 GOP 粒度去丢弃历史久远的 NACK 请求项。
下面举例说明。假设有一个视频流,每个 GOP 由 5 个非 I 帧 报文和 2 个 I 帧报文组成,报文序列如下所示:
1,2,3,4,5,6,7,8,9,10,11,12,13,14,...
如果没有及时收到 3、4、11、13 四个报文,NackList 和 KeyFrameList 状态如下:
此时,如果需要创建新的 NACK 项,但 NackList 空间不够,需要丢弃 GOP1(3和4两个Nack项),状态如下:
NackList 空出两个表项,如果空间还不够,则从 KeyFrameList 中弹出表项,直到 SN 比 NackList 中的大,然后重复删除过程。
3.2.3. RecoveredList
NackRequester 内部有一个 RecoveredList,如果收到的是通过 FEC 或 RTX 恢复的报文,不会用来生成 NACK 请求项,而是被保存到 RecoveredList 中。在创建 NACK 请求项时,如果此报文已经被恢复了,则需要跳过。
为什么不把恢复报文当成普通的报文来处理,目前看是如果那样做会影响乱序的统计,而乱序的统计,又会影响前面讲到的 kSeqNumOnly 快速重传序号的计算。
3.3. 源码分析
3.3.1. OnReceivedPacket
这是 NackRequester 主函数,收到每个报文都需要调用此函数来生成或移除 NACK 请求项。
int NackRequester::OnReceivedPacket(uint16_t seq_num, bool is_keyframe,bool is_recovered) {bool is_retransmitted = true;// 初始化if (!initialized_) {newest_seq_num_ = seq_num;if (is_keyframe)keyframe_list_.insert(seq_num);initialized_ = true;return 0;}// 重复接收if (seq_num == newest_seq_num_)return 0;// 乱序包if (AheadOf(newest_seq_num_, seq_num)) {auto nack_list_it = nack_list_.find(seq_num);int nacks_sent_for_packet = 0;// 报文已经收到,移除 nack 项if (nack_list_it != nack_list_.end()) {nacks_sent_for_packet = nack_list_it->second.retries;nack_list_.erase(nack_list_it);}// 直方图统计乱序情况,重传报文的乱序不统计if (!is_retransmitted)UpdateReorderingStatistics(seq_num);return nacks_sent_for_packet;}// 保存关键帧报文序列号if (is_keyframe)keyframe_list_.insert(seq_num);// 关键帧报文太多了,清理下auto it = keyframe_list_.lower_bound(seq_num - kMaxPacketAge);if (it != keyframe_list_.begin())keyframe_list_.erase(keyframe_list_.begin(), it);// 经 FEC 或 RTX 恢复的报文if (is_recovered) {recovered_list_.insert(seq_num);// 恢复报文太多,清理下auto it = recovered_list_.lower_bound(seq_num - kMaxPacketAge);if (it != recovered_list_.begin())recovered_list_.erase(recovered_list_.begin(), it);// Do not send nack for packets recovered by FEC or RTX.return 0;}// 走到这里 seq_num 肯定比 newest_seq_num 大,newest_seq_num_ + 1, seq_num 之间// 可能存在 0 个或多个空洞,这些空洞就是需要发送nack的报文AddPacketsToNack(newest_seq_num_ + 1, seq_num);// 更新收到的最新序列号newest_seq_num_ = seq_num;// 这里仅发送基于序列号触发的 NACK 请求std::vector<uint16_t> nack_batch = GetNackBatch(kSeqNumOnly);if (!nack_batch.empty()) {nack_sender_->SendNack(nack_batch, /*buffering_allowed=*/true);}return 0;
}
3.3.2. AddPacketsToNack
当新收到报文与最近收的报文之间有空洞时,会调用此函数插入 NACK 请求项。这里要关注下,极端情况会清空 NACK 请求列表,直接发送关键帧请求。
void NackRequester::AddPacketsToNack(uint16_t seq_num_start, uint16_t seq_num_end) {// NACK 项太多了,清理下auto it = nack_list_.lower_bound(seq_num_end - kMaxPacketAge);nack_list_.erase(nack_list_.begin(), it);// 计算空洞数量uint16_t num_new_nacks = ForwardDiff(seq_num_start, seq_num_end);// 确保添加空洞 NACK 项后总 NACK 项不会超过最大限制if (nack_list_.size() + num_new_nacks > kMaxNackPackets) {// 先移除关键帧之前的 NACK 项while (RemovePacketsUntilKeyFrame() &&nack_list_.size() + num_new_nacks > kMaxNackPackets) {}// 还是腾不出足够的空间,则清空 NACK 队列,直接请求 I 帧if (nack_list_.size() + num_new_nacks > kMaxNackPackets) {nack_list_.clear();keyframe_request_sender_->RequestKeyFrame();return;}}// 遍历所有空洞创建 NACK 项for (uint16_t seq_num = seq_num_start; seq_num != seq_num_end; ++seq_num) {// 空洞报文可能已经被 FEC 或 RTX 恢复if (recovered_list_.find(seq_num) != recovered_list_.end())continue;// 使用乱序长度的中位数来计算触发重传的序列号NackInfo nack_info(seq_num, seq_num + WaitNumberOfPackets(0.5), clock_->CurrentTime());nack_list_[seq_num] = nack_info;}
}
3.3.3. GetNackBatch
定时器驱动调用此函数,定时检查发送 NACK 请求项。
void NackRequester::ProcessNacks() {// 定时器驱动,只获取基于时间条件判断需要处理的 NACK 项std::vector<uint16_t> nack_batch = GetNackBatch(kTimeOnly);if (!nack_batch.empty()) {nack_sender_->SendNack(nack_batch, /*buffering_allowed=*/false);}
}std::vector<uint16_t> NackRequester::GetNackBatch(NackFilterOptions options) {// 仅考虑序列号bool consider_seq_num = options != kTimeOnly;// 仅考虑时间bool consider_timestamp = options != kSeqNumOnly;// 当前时间Timestamp now = clock_->CurrentTime();// 筛选结果std::vector<uint16_t> nack_batch;auto it = nack_list_.begin();while (it != nack_list_.end()) {// 等待发送 NACK 的时间已经到了bool delay_timed_out = now - it->second.created_at_time >= send_nack_delay_;// 距离上一次发送 NACK 的时间也已经过去很久了bool nack_on_rtt_passed = now - it->second.sent_at_time >= rtt_;// 基于序列号的发送,只有在第一次发送Nack时生效bool nack_on_seq_num_passed =it->second.sent_at_time.IsInfinite() &&AheadOrAt(newest_seq_num_, it->second.send_at_seq_num);// 已经过了等待时间,基于时间和基于序列号两者满足其一if (delay_timed_out && ((consider_seq_num && nack_on_seq_num_passed) ||(consider_timestamp && nack_on_rtt_passed))) {nack_batch.emplace_back(it->second.seq_num);++it->second.retries; // 更新发送 NACK 请求次数it->second.sent_at_time = now; // 更新最近发送 NACK 请求时间// 已经达到最大请求次数限制,从队列中移除,不再请求了if (it->second.retries >= kMaxNackRetries) {it = nack_list_.erase(it);} else {++it;}continue;}++it;}return nack_batch;
}
4. 总结
WebRTC NACK 的实现简单明了,发送端缓存报文,接收端请求重传。但发送端和接收端实现关注重点不太一样。发送端是被动接收 NACK 请求,实现相对简单一些,重点关注缓存队列的长度。接收端需要主动发送发送 NACK 请求,实现会相对复杂一些,由于存在报文乱序,什么时候发起 NACK 请求是一个值得斟酌的事情。
除此之外,WebRTC 还考虑到了瞬间突发丢包的快速重传机制和基于关键帧的队列收缩等,这些都凸显了 WebRTC 对细节的掌控和重视。
相关文章:

深入浅出WebRTC—NACK
WebRTC 中的 NACK(Negative Acknowledgment)机制是实时通信中处理网络丢包的关键组件。网络丢包是常见的现象,尤其是在无线网络或不稳定连接中。NACK 机制旨在通过请求重传丢失的数据包来减少这种影响,从而保持通信的连续性和质量…...
简单工厂模式、工厂模式和抽象工厂模式的区别
简单工厂模式、工厂模式和抽象工厂模式都是创建型设计模式,它们之间在目的、实现方式和适用场景上存在显著的区别。以下是对这三种模式的详细比较: 一、定义与目的 简单工厂模式(Simple Factory Pattern) 定义: 简单工…...

JVM-垃圾回收与内存分配
目录 垃圾收集器与内存分配策略 引用 对象的访问方式有哪些?(句柄和直接指针) Java的引用有哪些类型? 如何判断对象是否是垃圾? 请列举一些可作为GC Roots的对象? 对象头了解吗? mark word(hashcode、分代、锁标志位)、…...

Jolt路线图
1. 引言 a16z crypto团队2024年7月更新了其Jolt路线图: 主要分为3大维度: 1)链上验证维度: 1.1)Zeromorph:见Aztec Labs团队2023年论文 Zeromorph: Zero-Knowledge Multilinear-Evaluation Proofs from…...
NEEP-EN2-2019-Text4
英二-2019-Text4摘自赫芬顿邮报《The Huffington Post》2018年6月的一篇名为“Let’s Stop Pretending Quitting Straws Will Solve Plastic Pollution”的文章。 以下为个人解析,非官方公开标准资料,可能有误,仅供参考。(单词解释…...

docker 部署wechatbot-webhook 并获取接口实现微信群图片自动保存到chevereto图库等
功能如图: docker部署 version: "3" services:excalidraw:image: dannicool/docker-wechatbot-webhook:latestcontainer_name: wechatbot-webhookdeploy:resources:limits:cpus: 0.15memory: 500Mreservations:cpus: 0.05memory: 80Mrestart: alwayspor…...
OpenWrt安装快速入门指南
在刷新 OpenWrt 固件之前,建议进行以下准备: 1、不要急于安装,慢慢来。如果在安装过程中出现奇怪之处,请先找到答案,然后再继续。 2、准备好设备的精确型号,以便能够选择正确的OpenWrt固件。 3、手上有关…...

AIGC Kolors可图IP-Adapter-Plus风格参考模型使用案例
参考: https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-Plus 代码环境安装: git clone https://github.com/Kwai-Kolors/Kolors cd Kolors conda create --name kolors python=3.8 conda activate kolors pip install -r requirements.txt python3 setup.py install…...

从零开始学量化~Ptrade使用教程(七)——期权相关操作
期权交易 可点击证券代码右侧的选,进入期权选择菜单。通过选择标的商品,认购期权和认沽期权中间的选项(包括代码、成交价、幅度%、隐波%、内在价值、时间价值等),以及认购期权或认沽期权,选择所需的期权标的…...

TeamViewer关闭访问密码或固定一组密码不变
TeamViewer的新UI界面变化较大,网上的一些信息已经不再有效,更新后的访问密码在如下图所示: 演示的版本为7.21.4—— 设置每次你的设备访问的密码...

iMazing 3 换手机后苹果游戏数据还有吗 换iPhone怎么转移游戏数据
当你想要更换手机,无论是选择升级到最新款iPhone,或者换到“经典”旧款iPhone,单机游戏数据的转移总是让人发愁。本文将详细介绍换手机后苹果游戏数据还有吗,以及换iPhone怎么转移游戏数据,确保你能无缝继续你的游戏体…...
正则表达式:电子邮件地址的格式详解,及常见正则表达式符号的详细解释和匹配方式
一、第一部分是对该段电子邮件的详解 var Regex /^(?:\w\.?)*\w(?:\w\.)*\w$/; 1.^:这个符号表示匹配输入字符串的开始位置。 2.(?:...):这是一个非捕获组(non-capturing group),用于将正则表达式的一部分组合在…...
AWS全服务历史年表:发布日期、GA和服务概述一览(一)
我一直在尝试从各种角度撰写关于Amazon Web Services(AWS)的信息和魅力。由于我喜欢技术历史,这次我总结了AWS服务发布的历史年表。 虽然AWS官方也通过“Whats New”发布了官方公告,但我一直希望能有一篇文章将公告日期、GA日期&…...

现场可重构CPLD芯片应用案例—蓝牙音箱
我司英尚微提供的高性能数模混合现场可重构IC、通用可配置的模数混合芯片内部集成丰富的模拟资源和数字资源,可轻松替代电路中的各种标准器件,并按照客户要求组合成最优小型ASIC,缩短开发周期,降低成本。下面介绍LS98002现场可重构…...
vue2关于Object.defineProperty实现响应式
实现步骤: 1. 初始化阶段 当 Vue 实例化时,会遍历data 选项中的属性,并使用 Object.defineProperty 将它们转换为 getter 和 setter。这样一来,每当访问或修改这些属性时, Vue就能捕获到这些操作,从而实现…...
中英双语介绍一级市场(Primary Market)和二级市场(Secondary Market)
中文版 一级市场和二级市场是金融市场中的两个主要部分,分别对应证券发行和交易的不同阶段。 一级市场(Primary Market) 定义: 一级市场,又称新发行市场,是指证券首次发行和出售的市场。在一级市场中&am…...
OpenCV 轮廓检测
在 OpenCV 中,轮廓检测是一种用于查找图像中具有相似颜色或强度的连通像素组的技术,这些像素组通常代表了图像中的物体边缘。轮廓可以用来识别和分割图像中的物体,是计算机视觉应用中的一个重要步骤,如目标识别、形状分析等。 轮…...

ubuntu源码安装Odoo
序言:时间是我们最宝贵的财富,珍惜手上的每个时分 Odoo具有非常多的安装方式,除了我最爱用的 apt-get install,我们还可以使用git拉取Odoo源码进行安装。 本次示例于ubuntu20.04 Desktop上进行操作,理论上在ubuntu14.04之后都可以用此操作。 …...

大鲸鱼docker-compose单机容器集群编排工具
目录 一、Docker-compose 概述 二、Docker-compose简介 三、YML文件格式及编写注意事项 1.yml文件是什么 2.yml问价使用注意事项 3.yml文件的基本数据结构 四、Docker-compose 配置 1.Docker-Compose 配置常用字段 2.Docker Compose常用命令 3.使用Docker-compose创建…...

Dify中的高质量索引模式实现过程
思考在什么情况下会使用到高质量索引模式呢?第1种情况是在知识库中上传文档,文档被拆分为段落后需要进行编码(增加);第2种情况是在召回测试的时候,需要对query进行编码(查询);第3种情况是当文档中的段落增加和更新时需要进行编码(增加和更新)。索引模式是针对知识库…...

第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...

idea大量爆红问题解决
问题描述 在学习和工作中,idea是程序员不可缺少的一个工具,但是突然在有些时候就会出现大量爆红的问题,发现无法跳转,无论是关机重启或者是替换root都无法解决 就是如上所展示的问题,但是程序依然可以启动。 问题解决…...

C++初阶-list的底层
目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...
laravel8+vue3.0+element-plus搭建方法
创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...

微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据 Power Query 具有大量专门帮助您清理和准备数据以供分析的功能。 您将了解如何简化复杂模型、更改数据类型、重命名对象和透视数据。 您还将了解如何分析列,以便知晓哪些列包含有价值的数据,…...

10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...