当前位置: 首页 > news >正文

C++造轮子飙车现场之无锁、有锁环形队列实现

先看带锁的实现。

带锁版本

circular_queue.h

// 头文件防卫
#ifndef CIRCULAR_QUEUE_H
#define CIRCULAR_QUEUE_H#include <mutex> // 互斥量
#include <condition_variable> // 条件变量template <typename T>
class CircularQueue {
public:// 构造函数,初始化成员变量explicit CircularQueue(size_t capacity) :capacity_(capacity),size_(0),head_(0),tail_(0),buffer_(new T[capacity]) {}// 析构函数,释放 buffer_ 内存~CircularQueue() {delete[] buffer_;}// 判断队列是否为空bool empty() {std::unique_lock<std::mutex> lock(mutex_);return size_ == 0;}// 判断队列是否已满bool full() {std::unique_lock<std::mutex> lock(mutex_);return size_ == capacity_;}// 获取队列中元素的数量size_t size() {std::unique_lock<std::mutex> lock(mutex_);return size_;}// 获取队列的容量size_t capacity() {return capacity_;}// 将元素加入队列,可能会阻塞bool push(const T& value, bool block = true) {std::unique_lock<std::mutex> lock(mutex_);if (block) {// 如果队列已满,则等待队列不满while (size_ == capacity_) {not_full_.wait(lock);}} else {// 如果队列已满,则返回 falseif (size_ == capacity_) {return false;}}// 将元素加入队列尾部,并更新 tail_ 和 size_buffer_[tail_] = value;tail_ = (tail_ + 1) % capacity_;++size_;// 通知一个等待在 not_empty_ 条件变量上的线程not_empty_.notify_one();return true;}// 将元素加入队列,可能会阻塞,使用右值引用bool push(T&& value, bool block = true) {std::unique_lock<std::mutex> lock(mutex_);if (block) {// 如果队列已满,则等待队列不满while (size_ == capacity_) {not_full_.wait(lock);}} else {// 如果队列已满,则返回 falseif (size_ == capacity_) {return false;}}// 将元素加入队列尾部,并更新 tail_ 和 size_buffer_[tail_] = std::move(value);tail_ = (tail_ + 1) % capacity_;++size_;// 通知一个等待在 not_empty_ 条件变量上的线程not_empty_.notify_one();return true;}// 从队列中取出元素,可能会阻塞bool pop(T& value, bool block = true) {std::unique_lock<std::mutex> lock(mutex_);if (block) {// 如果队列为空,则等待队列不空while (size_ == 0) {not_empty_.wait(lock);}} else {// 如果队列为空,则返回 falseif (size_ == 0) {return false;}}// 取出队列头部元素,并更新 head_ 和 size_value = std::move(buffer_[head_]);head_ = (head_ + 1) % capacity_;--size_;// 通知一个等待在 not_full_ 条件变量上的线程not_full_.notify_one();return true;}private:const size_t capacity_; // 队列容量size_t size_; // 队列中元素的数量size_t head_; // 队列头部指针size_t tail_; // 队列尾部指针T* buffer_; // 队列缓冲区    std::mutex mutex_; // 互斥量,保护队列缓冲区和队列大小std::condition_variable not_full_; // 条件变量,当队列满时等待std::condition_variable not_empty_; // 条件变量,当队列空时等待
};#endif // CIRCULAR_QUEUE_H

push和pop接口不指定第二个参数的话,默认是阻塞的,这一点使用时需要注意。

以下是CircularQueue类的单元测试示例代码:

#include <gtest/gtest.h>
#include <thread>#include "circular_queue.h"TEST(CircularQueueTest, EmptyQueue) {CircularQueue<int> queue(10);ASSERT_TRUE(queue.empty());ASSERT_FALSE(queue.full());ASSERT_EQ(queue.size(), 0);ASSERT_EQ(queue.capacity(), 10);
}TEST(CircularQueueTest, PushAndPop) {CircularQueue<int> queue(3);ASSERT_TRUE(queue.push(1));ASSERT_EQ(queue.size(), 1);ASSERT_FALSE(queue.empty());ASSERT_FALSE(queue.full());ASSERT_TRUE(queue.push(2));ASSERT_EQ(queue.size(), 2);ASSERT_FALSE(queue.empty());ASSERT_FALSE(queue.full());ASSERT_TRUE(queue.push(3));ASSERT_EQ(queue.size(), 3);ASSERT_FALSE(queue.empty());ASSERT_TRUE(queue.full());int value;ASSERT_TRUE(queue.pop(value));ASSERT_EQ(value, 1);ASSERT_EQ(queue.size(), 2);ASSERT_FALSE(queue.empty());ASSERT_FALSE(queue.full());ASSERT_TRUE(queue.pop(value));ASSERT_EQ(value, 2);ASSERT_EQ(queue.size(), 1);ASSERT_FALSE(queue.empty());ASSERT_FALSE(queue.full());ASSERT_TRUE(queue.pop(value));ASSERT_EQ(value, 3);ASSERT_EQ(queue.size(), 0);ASSERT_TRUE(queue.empty());ASSERT_FALSE(queue.full());ASSERT_FALSE(queue.pop(value,false));
}TEST(CircularQueueTest, PushAndPopWithBlocking) {CircularQueue<int> queue(2);std::thread t([&queue]() {int value = 0;queue.pop(value);ASSERT_EQ(value, 1);queue.pop(value);ASSERT_EQ(value, 2);});ASSERT_TRUE(queue.push(1));ASSERT_TRUE(queue.push(2));ASSERT_TRUE(queue.push(3));t.join();
}TEST(CircularQueueTest, PushAndPopWithNonBlocking) {CircularQueue<int> queue(2);int value;ASSERT_TRUE(queue.push(1));ASSERT_TRUE(queue.push(2));ASSERT_FALSE(queue.push(3, false));ASSERT_TRUE(queue.pop(value));ASSERT_EQ(value, 1);ASSERT_TRUE(queue.pop(value));ASSERT_EQ(value, 2);ASSERT_FALSE(queue.pop(value, false));
}TEST(CircularQueueTest, MovePushAndPop) {CircularQueue<std::string> queue(3);ASSERT_TRUE(queue.push("hello"));ASSERT_TRUE(queue.push("world"));ASSERT_EQ(queue.size(), 2);std::string value;ASSERT_TRUE(queue.pop(value));ASSERT_EQ(value, "hello");ASSERT_EQ(queue.size(), 1);ASSERT_TRUE(queue.push("foo"));ASSERT_EQ(queue.size(), 2);ASSERT_TRUE(queue.pop(value));ASSERT_EQ(value, "world");ASSERT_EQ(queue.size(), 1);ASSERT_TRUE(queue.pop(value));ASSERT_EQ(value, "foo");ASSERT_EQ(queue.size(), 0);
}TEST(CircularQueueTest, CopyPushAndPop) {CircularQueue<std::string> queue(3);ASSERT_TRUE(queue.push(std::string("hello")));ASSERT_TRUE(queue.push(std::string("world")));ASSERT_EQ(queue.size(), 2);std::string value;ASSERT_TRUE(queue.pop(value));ASSERT_EQ(value, "hello");ASSERT_EQ(queue.size(), 1);ASSERT_TRUE(queue.push(std::string("foo")));ASSERT_EQ(queue.size(), 2);ASSERT_TRUE(queue.pop(value));ASSERT_EQ(value, "world");ASSERT_EQ(queue.size(), 1);ASSERT_TRUE(queue.pop(value));ASSERT_EQ(value, "foo");ASSERT_EQ(queue.size(), 0);
}TEST(CircularQueueTest, MultiThreadPushPop) {const int num_threads = 4;const int num_iterations = 10000;const int queue_size = 100;CircularQueue<int> queue(queue_size);std::vector<std::thread> threads;for (int i = 0; i < num_threads; ++i) {threads.emplace_back([&queue, num_iterations]() {for (int j = 0; j < num_iterations; ++j) {queue.push(j);}});}for (int i = 0; i < num_threads; ++i) {threads.emplace_back([&queue, num_iterations]() {for (int j = 0; j < num_iterations; ++j) {int value;queue.pop(value);}});}for (auto& thread : threads) {thread.join();}ASSERT_EQ(queue.size(), 0);
}int main(int argc, char** argv) {testing::InitGoogleTest(&argc, argv);return RUN_ALL_TESTS();
}

单元测试运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CqCh8DqE-1679157650605)(C:\Users\小静静\AppData\Roaming\Typora\typora-user-images\image-20230319001514719.png)]

无锁版本

上面的循环队列使用锁保证了线程安全。

以下是一个基于C++11的线程安全且无锁的环形队列实现,支持阻塞读和非阻塞读,写也一样。

#include <atomic>
#include <condition_variable>
#include <functional>
#include <iostream>
#include <thread>
#include <vector>template <typename T, size_t N>
class RingQueue {
public:RingQueue() : read_idx_(0), write_idx_(0), data_{} {}bool Push(const T& item, bool block = false) { return PushImpl(item, block); }bool Push(T&& item, bool block = false) { return PushImpl(std::move(item), block); }bool Pop(T& item, bool block = false) {// 只有一个读者,read_idx_的读取可以不加锁size_t current_read_idx = read_idx_.load(std::memory_order_relaxed);// 保证读到write_idx_的变化,此处memory_order_acquire发挥的是可见性问题while (current_read_idx == write_idx_.load(std::memory_order_acquire)) {if (!block) {return false;}std::this_thread::yield();}item = std::move(data_[current_read_idx]); // 必须先把数据读走read_idx_才能+1,memory_order_release保证了对item的写不会被重排到read_idx_ + 1之后read_idx_.store(Next(current_read_idx), std::memory_order_release);return true;}template <typename Func>bool Pop(Func&& func, bool block = false) {size_t current_read_idx = read_idx_.load(std::memory_order_relaxed);while (current_read_idx == write_idx_.load(std::memory_order_acquire)) {if (!block) {return false;}std::this_thread::yield();}T item = std::move(data_[current_read_idx]);read_idx_.store(Next(current_read_idx), std::memory_order_release);func(std::move(item));return true;}void PopAsync(const T& value, std::function<void(bool)> callback) {auto task = [this, value, callback]() {bool result = Pop(value, true);callback(result);};std::thread(std::move(task)).detach();}    bool IsEmpty() const {return read_idx_.load(std::memory_order_acquire) ==write_idx_.load(std::memory_order_acquire);}bool IsFull() const {return Next(write_idx_.load(std::memory_order_acquire)) ==read_idx_.load(std::memory_order_acquire);}private:template <typename Item>bool PushImpl(Item&& item, bool block = false) {// 只有1个写线程, 所以write_idx_可以不加锁size_t current_write_idx = write_idx_.load(std::memory_order_relaxed);size_t next_write_idx = Next(current_write_idx);// 读线程会修改read_idx_, 所以此处需要保证看到read_idx_的变化,此处memory_order_acquire保证的是可见性问题while (next_write_idx == read_idx_.load(std::memory_order_acquire)) {if(!block) {return false;}std::this_thread::yield();}// 数据的写入必须在write_idx_+1之前data_[current_write_idx] = std::forward<Item>(item);// 保证之前的写操作对读线程可见,即读线程能立刻看到data_刚写入的数据,当然也包括write_idx_的+1变化,memory_order_release会保证对data_的写入在write_idx_+1的操作之前完成。// 因为就算data_的赋值语句放在write_idx_+1之前,由于编译器或者运行期指令重排,并不一定能保证data_赋值语句就一定在write_idx_+1前执行。write_idx_.store(next_write_idx, std::memory_order_release);return true;}size_t Next(size_t current_idx) const { return (current_idx + 1) % (N+1); } // 此处笔者做了修改,N改成N+1std::atomic<size_t> read_idx_;std::atomic<size_t> write_idx_;std::array<T, N+1> data_;  // 此处笔者做了修改,N改成N+1
};

代码中使用了模板参数T和N,支持了不同数据类型和不同队列大小的选择,借助读写指针两个原子变量实现无锁环形队列。

但需要注意的是,这个只能实现一读一写的线程安全,存在多个读者或者多个写者时就线程不安全了。

无锁编程的难点在于对几个内存时序的理解。

补充下关于内存时序操作的解释。

C++定义了几种内存时序,这些时序规定了原子变量前后的所有内存操作(包括普通变量、原子变量)如何排序

std::memory_order_relaxed只保正操作的原子性,对于同一个原子变量的多个操作之间不存在任何内存序的限制,也就是说,它们可以随意重排序,也可以在任意时刻被其他线程的操作所干扰。因此,使用std::memory_order_relaxed时需要特别小心,必须确保操作的正确性不受此种松散的内存访问顺序的影响。

std::memory_order_relaxed主要用于那些不需要任何同步机制的场合,比如计数器的自增、自减等操作,这些操作只需要保证结果的正确性,而不需要保证其执行的顺序。因此,std::memory_order_relaxed是最快的内存序,但也是最危险的一种内存序。


std::memory_order_acquire确保所有之前的读操作都已经完成,然后再执行当前读取操作。这意味着,如果当前读取操作需要用到之前的读取操作的结果,那么它将能够正确地获取到这些结果。

具体来说,当使用memory_order_acquire语义时,编译器和处理器都会保证当前线程所在的CPU核心(或处理器)在执行当前原子操作之前,会先将所有之前的读操作所获得的数据从CPU缓存中刷新到主内存中,以保证当前线程能够读取到其他线程对共享变量的最新修改。

使用memory_order_acquire语义可以保证程序的正确性,避免出现数据竞争的问题。但是,使用memory_order_acquire语义可能会降低程序的性能,因为它要求在执行原子操作之前,必须将所有之前的读操作都刷新到主内存中,这可能会导致缓存一致性协议的开销增加。因此,在实际编程中,应该根据具体情况选择合适的内存序语义。


std::memory_order_release确保当前线程的所有写操作在该原子操作之前都已经完成,并且将这些写操作对其他线程可见。这样,其他线程就可以看到当前线程对共享数据所做的更改。

这种释放操作通常用于同步操作,例如将一个共享变量的值更新后通知其他线程。在这种情况下,std::memory_order_release可以确保其他线程能够看到更新后的值。

Push提供了插入左值和右值两个版本,提高C++所谓的一点点性能,PushImpl提取了公共代码,实现代码复用,优雅!。

Pop提供了两个版本,有个变体,不返回pop出来的对象,而是调用外部传入的回调,对pop出来的对象进行操作。

同样附上单元测试。

#include <gtest/gtest.h>
#include "RingQueue.h"class RingQueueSingleThreadTest : public ::testing::Test {
protected:RingQueue<int, 10> queue_;
};TEST_F(RingQueueSingleThreadTest, PushAndPop) {int value = 0;EXPECT_FALSE(queue_.Pop(value));EXPECT_TRUE(queue_.Push(1));EXPECT_FALSE(queue_.IsEmpty());EXPECT_TRUE(queue_.Pop(value));EXPECT_EQ(value, 1);EXPECT_TRUE(queue_.IsEmpty());
}TEST_F(RingQueueSingleThreadTest, PushAndPopWithBlock) {int value = 0;std::thread t([&](){std::this_thread::sleep_for(std::chrono::milliseconds(100));queue_.Push(1, true);});EXPECT_TRUE(queue_.Pop(value, true));EXPECT_EQ(value, 1);t.join();
}TEST_F(RingQueueSingleThreadTest, PushAndPopWithFunc) {int value = 0;queue_.Push(1);queue_.Pop([&](int v){ value = v + 1; });EXPECT_EQ(value, 2);
}TEST_F(RingQueueSingleThreadTest, IsEmptyAndIsFull) {EXPECT_TRUE(queue_.IsEmpty());EXPECT_FALSE(queue_.IsFull());for (int i = 0; i < 10; ++i) {EXPECT_TRUE(queue_.Push(i));}EXPECT_TRUE(queue_.IsFull());EXPECT_FALSE(queue_.IsEmpty());int value = 0;EXPECT_FALSE(queue_.Push(10));EXPECT_TRUE(queue_.Pop(value));EXPECT_FALSE(queue_.IsFull());
}class RingQueueMultiThreadTest : public testing::Test {
protected:virtual void SetUp() {// 初始化数据for (int i = 0; i < 1000; ++i) {data_.push_back("data_" + std::to_string(i));}}std::vector<std::string> data_;
};TEST_F(RingQueueMultiThreadTest, MultiThreadTest) {RingQueue<std::string, 10> queue;// 写线程std::thread writer([&queue, this]() {for (const auto& item : data_) {queue.Push(item, true);}});// 读线程std::thread reader([&queue, this]() {int count = 0;std::string item;while (count < 1000) {if (queue.Pop(item, true)) {EXPECT_EQ(item, "data_" + std::to_string(count));++count;} else {std::this_thread::yield();}}});writer.join();reader.join();
}int main(int argc, char** argv) {::testing::InitGoogleTest(&argc, argv);return RUN_ALL_TESTS();
}

在这里插入图片描述

相关文章:

C++造轮子飙车现场之无锁、有锁环形队列实现

先看带锁的实现。 带锁版本 circular_queue.h // 头文件防卫 #ifndef CIRCULAR_QUEUE_H #define CIRCULAR_QUEUE_H#include <mutex> // 互斥量 #include <condition_variable> // 条件变量template <typename T> class CircularQueue { public:// 构造函数…...

Spring Profiles and @Profile

1. Overview In this tutorial, we’ll focus on introducing Profiles in Spring. Profiles are a core feature of the framework — allowing us to map our beans to different profiles — for example, dev, test, and prod. We can then activate different profiles…...

数据分析-数据探索

文章目录前言主要内容总结更多宝藏前言 &#x1f60e;&#x1f973;&#x1f60e;&#x1f920;&#x1f62e;&#x1f916;&#x1f648;&#x1f4ad;&#x1f373;&#x1f371; 随着大数据和人工智能技术的不断发展&#xff0c;数据分析已经成为了一种非常重要的技能和工…...

7个最受欢迎的Python库,大大提高开发效率

当第三方库可以帮我们完成需求时&#xff0c;就不要重复造轮子了 整理了GitHub上7个最受好评的Python库&#xff0c;将在你的开发之旅中提供帮助 PySnooper 很多时候时间都花在了Debug上&#xff0c;大多数人呢会在出错位置的附近使用print&#xff0c;打印某些变量的值 这个…...

Intellij IDEA 中调试 maven 插件

Intellij IDEA 中调试 maven 插件话痨一下步骤1. classfinal-demo 项目部分2. ClassFinal 部分参考资料话痨一下 目前有两个项目&#xff1a; ClassFinal 是一款java class文件安全加密工具。classfinal-demo 是我建的一个Demo&#xff0c;用来测试ClassFinal的加密效果。 目…...

Java全栈知识(1)缓存池

我们先看这么一道题 Integer x new Integer(123); Integer y new Integer(123); System.out.println(x y); // false Integer z 123; Integer k 123; System.out.println(z k); // true Integer a 200; Integer b 200; System.out.println(z k); //false 我们…...

网络安全的特性

0x00 前言 网络安全的特性包括&#xff0c;机密性&#xff0c;完整性&#xff0c;可用性&#xff0c;真实性和不可否认性。详细的内容可以参考如下的内容。 Xmind资源请下载~ 0x01 机密性 机密性&#xff08;Confidentiality&#xff09; 意味着阻止未经授权的实体&#x…...

YOLOv8 多目标跟踪

文章大纲 简介环境搭建代码样例跟踪原理代码分析原始老版实现新版本封装代码实现追踪与计数奇奇怪怪错误汇总lap 安装过程报错推理过程报错参考文献与学习路径简介 使用yolov8 做多目标跟踪 文档地址: https://docs.ultralytics.com/modes/track/https://github.com/ultralyt…...

Gitee搭建个人博客(Beautiful Jekyll)

目录一、引言二、博客模板选型 - Jekyll三、安装Jekyll环境3.1 安装Ruby3.2 安装Jekyll3.3 下载Jekyll主题四、搭建我的Gitee博客4.1 选择主题 - Beautiful Jekyll4.2 创建Gitee账号同名代码库4.3 写博客4.4 开通Gitee Pages服务五、对Beautifu Jekyll的相关优化一、引言 之前…...

图形视图框架 事件处理(item)

在图形界面框架中的事件都是先由视图进行接收&#xff0c;然后传递给场景&#xff0c;再由场景传递给图形项。通过键盘处理的话&#xff0c;需要设置焦点&#xff0c;在QGraphicsScene中使用setFoucesItem&#xff08;&#xff09;函数可以设置焦点&#xff0c;或者图形项使用s…...

PTA第六章作业详解

&#x1f680;write in front&#x1f680; &#x1f4dd;个人主页&#xff1a;认真写博客的夏目浅石. &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd; &#x1f4e3;系列专栏&#xff1a;夏目的作业 &#x1f4ac;总结&#xff1a;希望你看完之后&am…...

Java课程设计项目--音乐视频网站系统

一、功能介绍 随着社会的快速发展&#xff0c;计算机的影响是全面且深入的。人们生活水平的不断提高&#xff0c;日常生活中人们对音乐方面的要求也在不断提高&#xff0c;听歌的人数更是不断增加&#xff0c;使得音乐网站的设计的开发成为必需而且紧迫的事情。音乐网站的设计主…...

FPGA可以转IC设计吗?需要学习哪些技能?

曾经在知乎上看到一个回答“入职做FPGA&#xff0c;后续是否还可以转数字IC设计&#xff1f;” 从下面图内薪资就可以对比出来&#xff0c;对比FPGA的行业薪资水平&#xff0c;IC行业中的一些基础性岗位薪资比很多FPGA大多数岗位薪资都要高。 除了薪资之外更多FPGA转IC设计的有…...

初探Gradle

目录一.概述二.优点三.安装与配置1. 官网下载2. 配置环境变量3. 检验4. 配置国内镜像(可选)5. IDEA配置三.工程结构四.生命周期1.Initialization阶段2.Configuration阶段3.Execution阶段五.Task六.常用任务指令七.引入依赖1.本地依赖2.项目依赖3.直接依赖八.依赖类型九.插件十.…...

国产数据库介绍

人大金仓 Kingbase 北京人大金仓信息技术股份有限公司于1999年由中共人民大学专家创立&#xff0c;自成立以来&#xff0c;始终立足自主研发&#xff0c;专注数据管理领域&#xff0c;先后承担了国家“863”、“核高基”等重大专项&#xff0c;研发出了具有国际先进水平的大型…...

Java OpenJudge-test3

目录 1:明明的随机数 2:合影效果 3:不重复的单词 4:和为给定数 5:字符串数组排序问题 6:字符串排序 7:求序列中的众数 1:明明的随机数 总时间限制: 1000ms 内存限制: 65536kB 描述 明明想在学校中请一些同学一起做一项问卷调查&#xff0c;为了实验的客观性&#xff…...

蓝桥杯刷题冲刺 | 倒计时22天

作者&#xff1a;指针不指南吗 专栏&#xff1a;蓝桥杯倒计时冲刺 &#x1f43e;马上就要蓝桥杯了&#xff0c;最后的这几天尤为重要&#xff0c;不可懈怠哦&#x1f43e; 文章目录1.选数异或2.特殊年份1.选数异或 题目 链接&#xff1a; 选数异或 - 蓝桥云课 (lanqiao.cn) 给定…...

入行 5年,跳槽 3次,我终于摸透了软件测试这行(来自过来人的忠告)

目录 前言 第一年 第二年 第三年 第四年 作为过来人的一些忠告 前言 最近几年行业在如火如荼的发展壮大&#xff0c;以及其他传统公司都需要大批量的软件测试人员&#xff0c;但是20年的疫情导致大规模裁员&#xff0c;让人觉得行业寒冬已来&#xff0c;软件测试人员的职…...

开源时序数据库学习

计划学习使用QuestDB解决大数据日志存储场景。以下是常见引擎比较 比较项目 InfluxDB TimescaleDB OpenTSDB QuestDB 数据模型 Key-Value Relational Key-Value Relational 存储引擎 自主开发的TSI PostgreSQL扩展程序 Apache HBase 自主开发 查询语言 InfluxQ…...

字节测试工程师悄悄告诉我的软件测试、测试开发常用的测试策略与测试手段

目录 前言 测试策略的关注重点 测试策略主要内容 总体测试策略 初级版本测试策略 跟踪测试执行 版本质量评估 后续版本测试策略 发布质量评估 测试手段 前言 测试策略是指在特定环境约束之下&#xff0c;描述软件开发周期中关于测试原则、方法、方式的纲要&#xff…...

线程与协程

1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指&#xff1a;像函数调用/返回一样轻量地完成任务切换。 举例说明&#xff1a; 当你在程序中写一个函数调用&#xff1a; funcA() 然后 funcA 执行完后返回&…...

视频字幕质量评估的大规模细粒度基准

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用&#xff0c;因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型&#xff08;VLMs&#xff09;在字幕生成方面…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现

摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序&#xff0c;以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务&#xff0c;提供稳定高效的数据处理与业务逻辑支持&#xff1b;利用 uniapp 实现跨平台前…...

【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验

系列回顾&#xff1a; 在上一篇中&#xff0c;我们成功地为应用集成了数据库&#xff0c;并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了&#xff01;但是&#xff0c;如果你仔细审视那些 API&#xff0c;会发现它们还很“粗糙”&#xff1a;有…...

Axios请求超时重发机制

Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式&#xff1a; 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...

【JavaSE】绘图与事件入门学习笔记

-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角&#xff0c;以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向&#xff0c;距离坐标原点x个像素;第二个是y坐标&#xff0c;表示当前位置为垂直方向&#xff0c;距离坐标原点y个像素。 坐标体系-像素 …...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

代理篇12|深入理解 Vite中的Proxy接口代理配置

在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...

中医有效性探讨

文章目录 西医是如何发展到以生物化学为药理基础的现代医学&#xff1f;传统医学奠基期&#xff08;远古 - 17 世纪&#xff09;近代医学转型期&#xff08;17 世纪 - 19 世纪末&#xff09;​现代医学成熟期&#xff08;20世纪至今&#xff09; 中医的源远流长和一脉相承远古至…...

【无标题】湖北理元理律师事务所:债务优化中的生活保障与法律平衡之道

文/法律实务观察组 在债务重组领域&#xff0c;专业机构的核心价值不仅在于减轻债务数字&#xff0c;更在于帮助债务人在履行义务的同时维持基本生活尊严。湖北理元理律师事务所的服务实践表明&#xff0c;合法债务优化需同步实现三重平衡&#xff1a; 法律刚性&#xff08;债…...